1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_api::UnitLength;
9use kcl_error::CompilationIssue;
10use kcl_error::SourceRange;
11use serde::Serialize;
12use uuid::Uuid;
13
14use crate::ExecOutcome;
15use crate::ExecutorContext;
16use crate::KclError;
17use crate::KclErrorWithOutputs;
18use crate::KclValueView;
19use crate::Program;
20use crate::SegmentDragAnchor;
21use crate::collections::AhashIndexSet;
22use crate::execution::Artifact;
23use crate::execution::ArtifactGraph;
24use crate::execution::ArtifactId;
25use crate::execution::CapSubType;
26use crate::execution::CodeRef;
27use crate::execution::MockConfig;
28use crate::execution::SKETCH_BLOCK_PARAM_ON;
29use crate::execution::annotations::WarningLevel;
30use crate::execution::cache::SketchModeState;
31use crate::execution::cache::clear_mem_cache;
32use crate::execution::cache::read_old_memory;
33use crate::execution::cache::write_old_memory;
34use crate::execution::types::adjust_length;
35use crate::fmt::format_number_literal;
36use crate::front::Angle;
37use crate::front::ArcCtor;
38use crate::front::ArcDirection;
39use crate::front::CircleCtor;
40use crate::front::ControlPointSplineCtor;
41use crate::front::Distance;
42use crate::front::EqualRadius;
43use crate::front::Error;
44use crate::front::ExecResult;
45use crate::front::FixedPoint;
46use crate::front::Freedom;
47use crate::front::LinesEqualLength;
48use crate::front::Midpoint;
49use crate::front::Object;
50use crate::front::Parallel;
51use crate::front::Perpendicular;
52use crate::front::PointCtor;
53use crate::front::Symmetric;
54use crate::front::Tangent;
55use crate::frontend::api::CapSource;
56use crate::frontend::api::Expr;
57use crate::frontend::api::FileId;
58use crate::frontend::api::Number;
59use crate::frontend::api::ObjectId;
60use crate::frontend::api::ObjectKind;
61use crate::frontend::api::Plane;
62use crate::frontend::api::ProjectId;
63use crate::frontend::api::RestoreSketchCheckpointOutcome;
64use crate::frontend::api::SceneGraph;
65use crate::frontend::api::SceneGraphDelta;
66use crate::frontend::api::SketchCheckpointId;
67use crate::frontend::api::SourceDelta;
68use crate::frontend::api::SourceRef;
69use crate::frontend::api::SourceRefRange;
70use crate::frontend::api::Version;
71use crate::frontend::api::WallSource;
72use crate::frontend::modify::find_defined_names;
73use crate::frontend::modify::next_free_name;
74use crate::frontend::modify::next_free_name_with_padding;
75use crate::frontend::sketch::Coincident;
76use crate::frontend::sketch::Constraint;
77use crate::frontend::sketch::ConstraintLabelPositionEdit;
78use crate::frontend::sketch::ConstraintSegment;
79use crate::frontend::sketch::Diameter;
80use crate::frontend::sketch::ExistingSegmentCtor;
81use crate::frontend::sketch::Horizontal;
82use crate::frontend::sketch::LineCtor;
83use crate::frontend::sketch::Point2d;
84use crate::frontend::sketch::Radius;
85use crate::frontend::sketch::Segment;
86use crate::frontend::sketch::SegmentCtor;
87use crate::frontend::sketch::SketchApi;
88use crate::frontend::sketch::SketchCtor;
89use crate::frontend::sketch::Vertical;
90use crate::id::IncIdGenerator;
91use crate::parsing::ast::types as ast;
92use crate::parsing::ast::types::BoxNode;
93use crate::parsing::ast::types::CallExpressionKw;
94use crate::parsing::ast::types::NodePathExt;
95use crate::pretty::NumericSuffix;
96use crate::std::constraints::LinesAtAngleKind;
97use crate::walk::NodeMut;
98use crate::walk::Visitable;
99use crate::walk::traverse::MutateBodyItem;
100use crate::walk::traverse::TraversalReturn;
101use crate::walk::traverse::Visitor;
102use crate::walk::traverse::dfs_mut;
103
104pub(crate) mod api;
105pub(crate) mod modify;
106pub(crate) mod sketch;
107
108pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
109
110#[derive(Debug, Clone)]
111struct SketchCheckpoint {
112 id: SketchCheckpointId,
113 source: SourceDelta,
114 program: Program,
115 scene_graph: SceneGraph,
116 exec_outcome: ExecOutcome,
117 point_freedom_cache: HashMap<ObjectId, Freedom>,
118 mock_memory: Option<SketchModeState>,
119}
120pub(crate) mod trim;
121
122struct ArcSizeConstraintParams {
123 points: Vec<ObjectId>,
124 function_name: &'static str,
125 value: f64,
126 units: NumericSuffix,
127 label_position: Option<Point2d<Number>>,
128 constraint_type_name: &'static str,
129}
130
131const POINT_FN: &str = "point";
132const POINT_AT_PARAM: &str = "at";
133const LINE_FN: &str = "line";
134const LINE_VARIABLE: &str = "line";
135const LINE_START_PARAM: &str = "start";
136const LINE_END_PARAM: &str = "end";
137const ARC_FN: &str = "arc";
138const ARC_VARIABLE: &str = "arc";
139const ARC_START_PARAM: &str = "start";
140const ARC_END_PARAM: &str = "end";
141const ARC_CENTER_PARAM: &str = "center";
142const ARC_DIRECTION_PARAM: &str = "direction";
143const ARC_DIRECTION_CW_NAME: &str = "CW";
145const CIRCLE_FN: &str = "circle";
146const CIRCLE_VARIABLE: &str = "circle";
147const CIRCLE_START_PARAM: &str = "start";
148const CIRCLE_CENTER_PARAM: &str = "center";
149const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
150const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
151const LABEL_POSITION_PARAM: &str = "labelPosition";
152
153const COINCIDENT_FN: &str = "coincident";
154const DIAMETER_FN: &str = "diameter";
155const DISTANCE_FN: &str = "distance";
156const FIXED_FN: &str = "fixed";
157const ANGLE_FN: &str = "angle";
158const ANGLE_DIMENSION_FN: &str = "angleDimension";
159const ANGLE_LINES_PARAM: &str = "lines";
160const ANGLE_SECTOR_PARAM: &str = "sector";
161const ANGLE_INVERSE_PARAM: &str = "inverse";
162const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
163const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
164const EQUAL_LENGTH_FN: &str = "equalLength";
165const EQUAL_RADIUS_FN: &str = "equalRadius";
166const HORIZONTAL_FN: &str = "horizontal";
167const MIDPOINT_FN: &str = "midpoint";
168const MIDPOINT_POINT_PARAM: &str = "point";
169const RADIUS_FN: &str = "radius";
170const SYMMETRIC_FN: &str = "symmetric";
171const SYMMETRIC_AXIS_PARAM: &str = "axis";
172const TANGENT_FN: &str = "tangent";
173const VERTICAL_FN: &str = "vertical";
174
175const LINE_PROPERTY_START: &str = "start";
176const LINE_PROPERTY_END: &str = "end";
177
178const ARC_PROPERTY_START: &str = "start";
179const ARC_PROPERTY_END: &str = "end";
180const ARC_PROPERTY_CENTER: &str = "center";
181const CIRCLE_PROPERTY_START: &str = "start";
182const CIRCLE_PROPERTY_CENTER: &str = "center";
183const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
184const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
185
186const CONSTRUCTION_PARAM: &str = "construction";
187
188#[derive(Debug, Clone, Copy)]
189enum EditDeleteKind {
190 Edit,
191 DeleteNonSketch,
192}
193
194struct ExecuteAfterEditOptions {
196 segment_ids_edited: AhashIndexSet<ObjectId>,
197 edit_kind: EditDeleteKind,
198 commit_solved_initial_guesses: bool,
199}
200
201impl EditDeleteKind {
202 fn is_delete(&self) -> bool {
204 match self {
205 EditDeleteKind::Edit => false,
206 EditDeleteKind::DeleteNonSketch => true,
207 }
208 }
209
210 fn to_change_kind(self) -> ChangeKind {
211 match self {
212 EditDeleteKind::Edit => ChangeKind::Edit,
213 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
214 }
215 }
216}
217
218#[derive(Debug, Clone, Copy)]
219enum ChangeKind {
220 Add,
221 Edit,
222 Delete,
223 None,
224}
225
226#[derive(Debug, Clone, Serialize, ts_rs::TS)]
227#[ts(export, export_to = "FrontendApi.ts")]
228#[serde(tag = "type")]
229pub enum SetProgramOutcome {
230 #[serde(rename_all = "camelCase")]
231 Success {
232 scene_graph: Box<SceneGraph>,
233 exec_outcome: Box<ExecOutcome>,
234 checkpoint_id: Option<SketchCheckpointId>,
235 },
236 #[serde(rename_all = "camelCase")]
237 ExecFailure { error: Box<KclErrorWithOutputs> },
238}
239
240pub struct EditSegmentsOptions {
242 pub anchor_segment_ids: Option<Vec<ObjectId>>,
248 pub drag_anchors: Vec<SegmentDragAnchor>,
251 pub constraint_label_edits: Vec<ConstraintLabelPositionEdit>,
254 pub commit_solved_initial_guesses: bool,
256}
257
258pub struct EditDistanceConstraintLabelPositionOptions {
260 pub anchor_segment_ids: Vec<ObjectId>,
262 pub commit_solved_initial_guesses: bool,
264}
265
266pub struct EditConstraintOptions {
268 pub commit_solved_initial_guesses: bool,
270}
271
272#[derive(Debug, Clone)]
273struct SolidAstReference {
274 variable_name: String,
275 output_index: Option<usize>,
276}
277
278#[derive(Debug, Clone)]
279pub struct FrontendState {
280 program: Program,
281 scene_graph: SceneGraph,
282 solid_references: HashMap<Uuid, SolidAstReference>,
284 point_freedom_cache: HashMap<ObjectId, Freedom>,
287 next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
290 next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
293 next_constraint_label_edits: Option<Vec<ConstraintLabelPositionEdit>>,
295 next_edit_commits_solver_solutions: Option<bool>,
299 sketch_checkpoints: VecDeque<SketchCheckpoint>,
300 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
301}
302
303impl Default for FrontendState {
304 fn default() -> Self {
305 Self::new()
306 }
307}
308
309impl FrontendState {
310 pub fn new() -> Self {
311 Self {
312 program: Program::empty(),
313 scene_graph: SceneGraph {
314 project: ProjectId(0),
315 file: FileId(0),
316 version: Version(0),
317 objects: Default::default(),
318 settings: Default::default(),
319 sketch_mode: Default::default(),
320 },
321 solid_references: HashMap::new(),
322 point_freedom_cache: HashMap::new(),
323 next_drag_anchor_segment_ids: None,
324 next_segment_drag_anchors: None,
325 next_constraint_label_edits: None,
326 next_edit_commits_solver_solutions: None,
327 sketch_checkpoints: VecDeque::new(),
328 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
329 }
330 }
331
332 pub fn scene_graph(&self) -> &SceneGraph {
334 &self.scene_graph
335 }
336
337 pub fn default_length_unit(&self) -> UnitLength {
338 self.program
339 .meta_settings()
340 .ok()
341 .flatten()
342 .map(|settings| settings.default_length_units)
343 .unwrap_or(UnitLength::Millimeters)
344 }
345
346 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
347 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
348
349 let checkpoint = SketchCheckpoint {
350 id: checkpoint_id,
351 source: SourceDelta {
352 text: source_from_ast(&self.program.ast),
353 },
354 program: self.program.clone(),
355 scene_graph: self.scene_graph.clone(),
356 exec_outcome,
357 point_freedom_cache: self.point_freedom_cache.clone(),
358 mock_memory: read_old_memory().await,
359 };
360
361 self.sketch_checkpoints.push_back(checkpoint);
362 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
363 self.sketch_checkpoints.pop_front();
364 }
365
366 Ok(checkpoint_id)
367 }
368
369 pub async fn edit_segments_with_options(
377 &mut self,
378 ctx: &ExecutorContext,
379 version: Version,
380 sketch: ObjectId,
381 segments: Vec<ExistingSegmentCtor>,
382 options: EditSegmentsOptions,
383 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
384 let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
385 self.next_drag_anchor_segment_ids
386 .replace(anchor_ids.into_iter().collect())
387 });
388 let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
389 let previous_constraint_label_edits = self.next_constraint_label_edits.replace(options.constraint_label_edits);
390 let previous_commit_mode = self
391 .next_edit_commits_solver_solutions
392 .replace(options.commit_solved_initial_guesses);
393 let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
394 if let Some(previous_anchor_ids) = previous_anchor_ids {
395 self.next_drag_anchor_segment_ids = previous_anchor_ids;
396 }
397 self.next_segment_drag_anchors = previous_drag_anchors;
398 self.next_constraint_label_edits = previous_constraint_label_edits;
399 self.next_edit_commits_solver_solutions = previous_commit_mode;
400 result
401 }
402
403 pub async fn edit_distance_constraint_label_position_with_options(
409 &mut self,
410 ctx: &ExecutorContext,
411 version: Version,
412 sketch: ObjectId,
413 constraint_id: ObjectId,
414 label_position: Point2d<Number>,
415 options: EditDistanceConstraintLabelPositionOptions,
416 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
417 let previous_commit_mode = self
418 .next_edit_commits_solver_solutions
419 .replace(options.commit_solved_initial_guesses);
420 let result = SketchApi::edit_distance_constraint_label_position(
421 self,
422 ctx,
423 version,
424 sketch,
425 constraint_id,
426 label_position,
427 options.anchor_segment_ids,
428 )
429 .await;
430 self.next_edit_commits_solver_solutions = previous_commit_mode;
431 result
432 }
433
434 pub async fn edit_distance_constraint_with_options(
436 &mut self,
437 ctx: &ExecutorContext,
438 version: Version,
439 sketch: ObjectId,
440 constraint_id: ObjectId,
441 constraint: Constraint,
442 options: EditConstraintOptions,
443 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
444 self.edit_constraint_with_options(ctx, version, sketch, constraint_id, constraint, options)
445 .await
446 }
447
448 pub async fn edit_angle_constraint_with_options(
450 &mut self,
451 ctx: &ExecutorContext,
452 version: Version,
453 sketch: ObjectId,
454 constraint_id: ObjectId,
455 angle: Angle,
456 options: EditConstraintOptions,
457 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
458 self.edit_constraint_with_options(ctx, version, sketch, constraint_id, Constraint::Angle(angle), options)
459 .await
460 }
461
462 pub async fn edit_constraint_with_options(
464 &mut self,
465 ctx: &ExecutorContext,
466 _version: Version,
467 sketch: ObjectId,
468 constraint_id: ObjectId,
469 constraint: Constraint,
470 options: EditConstraintOptions,
471 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
472 let sketch_block_ref =
474 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
475
476 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
477 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
478 })?;
479
480 let mut new_ast = self.program.ast.clone();
481 let command = match &object.kind {
482 ObjectKind::Constraint {
483 constraint:
484 Constraint::Distance(_) | Constraint::HorizontalDistance(_) | Constraint::VerticalDistance(_),
485 } => {
486 let (function_name, distance) = match &constraint {
487 Constraint::Distance(distance) => (DISTANCE_FN, distance),
488 Constraint::HorizontalDistance(distance) => (HORIZONTAL_DISTANCE_FN, distance),
489 Constraint::VerticalDistance(distance) => (VERTICAL_DISTANCE_FN, distance),
490 _ => {
491 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
492 "A distance constraint can only be replaced by another distance constraint".to_owned(),
493 )));
494 }
495 };
496 let (call, value) = self
497 .distance_constraint_ast_parts(function_name, distance, &mut new_ast)
498 .map_err(KclErrorWithOutputs::no_outputs)?;
499 AstMutateCommand::EditDistanceConstraint { call, value }
500 }
501 ObjectKind::Constraint {
502 constraint: Constraint::Angle(_),
503 } => {
504 let Constraint::Angle(angle) = &constraint else {
505 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
506 "An angle constraint can only be replaced by another angle constraint".to_owned(),
507 )));
508 };
509 let (call, value) = self
510 .angle_constraint_ast_parts(angle, &mut new_ast)
511 .map_err(KclErrorWithOutputs::no_outputs)?;
512 AstMutateCommand::EditAngleConstraint { call, value }
513 }
514 ObjectKind::Constraint { .. } => {
515 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
516 "Editing {} is not supported",
517 object.kind.human_friendly_kind_with_article(),
518 ))));
519 }
520 _ => {
521 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
522 "Object is not a constraint: {constraint_id:?}"
523 ))));
524 }
525 };
526
527 self.mutate_ast(&mut new_ast, constraint_id, command)
528 .map_err(KclErrorWithOutputs::no_outputs)?;
529
530 self.execute_after_edit(
531 ctx,
532 sketch,
533 sketch_block_ref,
534 &mut new_ast,
535 ExecuteAfterEditOptions {
536 segment_ids_edited: Default::default(),
537 edit_kind: EditDeleteKind::Edit,
538 commit_solved_initial_guesses: options.commit_solved_initial_guesses,
539 },
540 )
541 .await
542 }
543
544 pub async fn restore_sketch_checkpoint(
545 &mut self,
546 checkpoint_id: SketchCheckpointId,
547 ) -> api::Result<RestoreSketchCheckpointOutcome> {
548 let checkpoint = self
549 .sketch_checkpoints
550 .iter()
551 .find(|checkpoint| checkpoint.id == checkpoint_id)
552 .cloned()
553 .ok_or_else(|| Error {
554 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
555 })?;
556
557 self.program = checkpoint.program;
558 self.scene_graph = checkpoint.scene_graph.clone();
559 self.solid_references = solid_references_from_variables(&self.program.ast, &checkpoint.exec_outcome.variables);
560 self.point_freedom_cache = checkpoint.point_freedom_cache;
561 self.next_drag_anchor_segment_ids = None;
562 self.next_segment_drag_anchors = None;
563 self.next_constraint_label_edits = None;
564 self.next_edit_commits_solver_solutions = None;
565
566 if let Some(mock_memory) = checkpoint.mock_memory {
567 write_old_memory(mock_memory).await;
568 } else {
569 clear_mem_cache().await;
570 }
571
572 Ok(RestoreSketchCheckpointOutcome {
573 source_delta: checkpoint.source,
574 scene_graph_delta: SceneGraphDelta {
575 new_graph: self.scene_graph_for_ui(),
576 new_objects: Vec::new(),
577 invalidates_ids: true,
578 exec_outcome: checkpoint.exec_outcome,
579 },
580 })
581 }
582
583 pub fn clear_sketch_checkpoints(&mut self) {
584 self.sketch_checkpoints.clear();
585 }
586 fn scene_graph_for_ui(&self) -> SceneGraph {
587 let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
588 matches!(
589 object.kind,
590 ObjectKind::Segment {
591 segment: Segment::ControlPointSpline(_)
592 }
593 )
594 });
595
596 if !has_control_point_splines {
597 return self.scene_graph.clone();
598 }
599
600 let hidden_constraint_ids = self
601 .scene_graph
602 .objects
603 .iter()
604 .filter_map(|object| match &object.kind {
605 ObjectKind::Constraint {
606 constraint: Constraint::Coincident(coincident),
607 } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
608 Some(object.id)
609 }
610 _ => None,
611 })
612 .collect::<HashSet<_>>();
613
614 if hidden_constraint_ids.is_empty() {
615 return self.scene_graph.clone();
616 }
617
618 let mut scene_graph = self.scene_graph.clone();
619 for object in &mut scene_graph.objects {
620 match &mut object.kind {
621 ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
622 object.kind = ObjectKind::Nil;
623 }
624 ObjectKind::Sketch(sketch) => {
625 sketch
626 .constraints
627 .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
628 }
629 _ => {}
630 }
631 }
632
633 scene_graph
634 }
635}
636
637fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
638 let mut first_owner_id = None;
639 for segment_id in coincident.segment_ids() {
640 let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
641 return false;
642 };
643
644 match first_owner_id {
645 Some(first_owner_id) if first_owner_id != owner_id => return false,
646 Some(_) => {}
647 None => first_owner_id = Some(owner_id),
648 }
649 }
650
651 first_owner_id.is_some()
652}
653
654fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
655 let object = scene_graph.objects.get(segment_id.0)?;
656 let ObjectKind::Segment { segment } = &object.kind else {
657 return None;
658 };
659
660 match segment {
661 Segment::ControlPointSpline(_) => Some(segment_id),
662 Segment::Point(point) => point
663 .owner
664 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
665 Segment::Line(line) => line
666 .owner
667 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
668 _ => None,
669 }
670}
671
672fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
673 matches!(
674 scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
675 Some(ObjectKind::Segment {
676 segment: Segment::ControlPointSpline(_)
677 })
678 )
679}
680
681fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
682 let experimental_features_allowed = program
683 .meta_settings()
684 .ok()
685 .flatten()
686 .map(|settings| settings.experimental_features == WarningLevel::Allow)
687 .unwrap_or(false);
688 if experimental_features_allowed {
689 return Ok(program.clone());
690 }
691
692 program.change_experimental_features(Some(WarningLevel::Allow))
693}
694
695impl SketchApi for FrontendState {
696 async fn execute_mock(
697 &mut self,
698 ctx: &ExecutorContext,
699 _version: Version,
700 sketch: ObjectId,
701 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
702 let sketch_block_ref =
703 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
704
705 let mut truncated_program = self.program.clone();
706 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
707 .map_err(KclErrorWithOutputs::no_outputs)?;
708
709 let outcome = ctx
711 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
712 .await?;
713 let new_source = source_from_ast(&self.program.ast);
714 let src_delta = SourceDelta { text: new_source };
715 let outcome = self.update_state_after_exec(outcome, true);
717 let scene_graph_delta = SceneGraphDelta {
718 new_graph: self.scene_graph.clone(),
719 new_objects: Default::default(),
720 invalidates_ids: false,
721 exec_outcome: outcome,
722 };
723 Ok((src_delta, scene_graph_delta))
724 }
725
726 async fn new_sketch(
727 &mut self,
728 ctx: &ExecutorContext,
729 _project: ProjectId,
730 _file: FileId,
731 _version: Version,
732 args: SketchCtor,
733 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
734 let mut new_ast = self.program.ast.clone();
737 let mut plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &self.solid_references, &args.on)
739 .map_err(KclErrorWithOutputs::no_outputs)?;
740 let mut defined_names = find_defined_names(&new_ast);
741 let is_face_of_expr = matches!(
742 &plane_ast,
743 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
744 );
745 if is_face_of_expr {
746 let face_name = next_free_name_with_padding("face", &defined_names)
747 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
748 let face_decl = ast::VariableDeclaration::new(
749 ast::VariableDeclarator::new(&face_name, plane_ast),
750 ast::ItemVisibility::Default,
751 ast::VariableKind::Const,
752 );
753 new_ast
754 .body
755 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
756 face_decl,
757 ))));
758 defined_names.insert(face_name.clone());
759 plane_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(&face_name)));
760 }
761 let sketch_ast = ast::SketchBlock {
762 arguments: vec![ast::LabeledArg {
763 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
764 arg: plane_ast,
765 }],
766 body: Default::default(),
767 is_being_edited: false,
768 non_code_meta: Default::default(),
769 digest: None,
770 };
771 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
774 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
775 let sketch_decl = ast::VariableDeclaration::new(
776 ast::VariableDeclarator::new(
777 &sketch_name,
778 ast::Expr::SketchBlock(BoxNode::new(ast::Node::no_src(sketch_ast))),
779 ),
780 ast::ItemVisibility::Default,
781 ast::VariableKind::Const,
782 );
783 new_ast
784 .body
785 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
786 sketch_decl,
787 ))));
788 let new_source = source_from_ast(&new_ast);
790 let new_program = parse_frontend_mutation_source(
792 &new_source,
793 "Error parsing KCL source after adding sketch",
794 "No AST produced after adding sketch",
795 )?;
796
797 self.program = new_program.clone();
799
800 let outcome = ctx.run_with_caching(new_program.clone()).await?;
803 let freedom_analysis_ran = true;
804
805 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
806
807 let Some(sketch_id) = self
808 .scene_graph
809 .objects
810 .iter()
811 .filter_map(|object| match object.kind {
812 ObjectKind::Sketch(_) => Some(object.id),
813 _ => None,
814 })
815 .max_by_key(|id| id.0)
816 else {
817 return Err(KclErrorWithOutputs::from_error_outcome(
818 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
819 outcome,
820 ));
821 };
822 self.scene_graph.sketch_mode = Some(sketch_id);
824
825 let src_delta = SourceDelta { text: new_source };
826 let scene_graph_delta = SceneGraphDelta {
827 new_graph: self.scene_graph_for_ui(),
828 invalidates_ids: false,
829 new_objects: vec![sketch_id],
830 exec_outcome: outcome,
831 };
832 Ok((src_delta, scene_graph_delta, sketch_id))
833 }
834
835 async fn edit_sketch(
836 &mut self,
837 ctx: &ExecutorContext,
838 _project: ProjectId,
839 _file: FileId,
840 _version: Version,
841 sketch: ObjectId,
842 ) -> ExecResult<SceneGraphDelta> {
843 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
847 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
848 })?;
849 let ObjectKind::Sketch(_) = &sketch_object.kind else {
850 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
851 "Object is not a sketch, it is {}",
852 sketch_object.kind.human_friendly_kind_with_article()
853 ))));
854 };
855 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
856
857 self.scene_graph.sketch_mode = Some(sketch);
859
860 let mut truncated_program = self.program.clone();
862 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
863 .map_err(KclErrorWithOutputs::no_outputs)?;
864
865 let outcome = ctx
868 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
869 .await?;
870
871 let outcome = self.update_state_after_exec(outcome, true);
873 let scene_graph_delta = SceneGraphDelta {
874 new_graph: self.scene_graph_for_ui(),
875 invalidates_ids: false,
876 new_objects: Vec::new(),
877 exec_outcome: outcome,
878 };
879 Ok(scene_graph_delta)
880 }
881
882 async fn exit_sketch(
883 &mut self,
884 ctx: &ExecutorContext,
885 _version: Version,
886 sketch: ObjectId,
887 ) -> ExecResult<SceneGraph> {
888 #[cfg(not(target_arch = "wasm32"))]
890 let _ = sketch;
891 #[cfg(target_arch = "wasm32")]
892 if self.scene_graph.sketch_mode != Some(sketch) {
893 web_sys::console::warn_1(
894 &format!(
895 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
896 self.scene_graph.sketch_mode
897 )
898 .into(),
899 );
900 }
901 self.scene_graph.sketch_mode = None;
902
903 let outcome = ctx.run_with_caching(self.program.clone()).await?;
905
906 self.update_state_after_exec(outcome, false);
908
909 Ok(self.scene_graph_for_ui())
910 }
911
912 async fn delete_sketch(
913 &mut self,
914 ctx: &ExecutorContext,
915 _version: Version,
916 sketch: ObjectId,
917 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
918 let mut new_ast = self.program.ast.clone();
921
922 let sketch_id = sketch;
924 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
925 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
926 })?;
927 let ObjectKind::Sketch(_) = &sketch_object.kind else {
928 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
929 "Object is not a sketch, it is {}",
930 sketch_object.kind.human_friendly_kind_with_article(),
931 ))));
932 };
933
934 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
936 .map_err(KclErrorWithOutputs::no_outputs)?;
937
938 self.execute_after_delete_sketch(ctx, &mut new_ast).await
939 }
940
941 async fn add_segment(
942 &mut self,
943 ctx: &ExecutorContext,
944 _version: Version,
945 sketch: ObjectId,
946 segment: SegmentCtor,
947 _label: Option<String>,
948 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
949 match segment {
951 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
952 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
953 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
954 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
955 SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
956 }
957 }
958
959 async fn edit_segments(
960 &mut self,
961 ctx: &ExecutorContext,
962 _version: Version,
963 sketch: ObjectId,
964 segments: Vec<ExistingSegmentCtor>,
965 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
966 let sketch_block_ref =
968 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
969
970 let mut new_ast = self.program.ast.clone();
971 let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
972 let mut invalidates_ids = false;
973
974 for segment in &segments {
977 edited_segment_ids.insert(segment.id);
978 if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
979 && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
980 && let ObjectKind::Segment {
981 segment: Segment::ControlPointSpline(existing_spline),
982 } = &existing_object.kind
983 && existing_spline.controls.len() != new_ctor.points.len()
984 {
985 invalidates_ids = true;
986 }
987 }
988 let drag_anchor_segment_ids = self
989 .next_drag_anchor_segment_ids
990 .take()
991 .unwrap_or_else(|| edited_segment_ids.clone());
992 let constraint_label_edits = self.next_constraint_label_edits.take().unwrap_or_default();
993 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
994
995 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
1010
1011 for segment in segments {
1012 let segment_id = segment.id;
1013 match segment.ctor {
1014 SegmentCtor::Point(ctor) => {
1015 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
1017 && let ObjectKind::Segment { segment } = &segment_object.kind
1018 && let Segment::Point(point) = segment
1019 && let Some(owner_id) = point.owner
1020 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1021 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1022 {
1023 match owner_segment {
1024 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
1025 if let Some(existing) = final_edits.get_mut(&owner_id) {
1026 let SegmentCtor::Line(line_ctor) = existing else {
1027 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1028 "Internal: Expected line ctor for owner, but found {}",
1029 existing.human_friendly_kind_with_article()
1030 ))));
1031 };
1032 if line.start == segment_id {
1034 line_ctor.start = ctor.position;
1035 } else {
1036 line_ctor.end = ctor.position;
1037 }
1038 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
1039 let mut line_ctor = line_ctor.clone();
1041 if line.start == segment_id {
1042 line_ctor.start = ctor.position;
1043 } else {
1044 line_ctor.end = ctor.position;
1045 }
1046 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
1047 } else {
1048 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1050 "Internal: Line does not have line ctor, but found {}",
1051 line.ctor.human_friendly_kind_with_article()
1052 ))));
1053 }
1054 continue;
1055 }
1056 Segment::Arc(arc)
1057 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
1058 {
1059 if let Some(existing) = final_edits.get_mut(&owner_id) {
1060 let SegmentCtor::Arc(arc_ctor) = existing else {
1061 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1062 "Internal: Expected arc ctor for owner, but found {}",
1063 existing.human_friendly_kind_with_article()
1064 ))));
1065 };
1066 if arc.start == segment_id {
1067 arc_ctor.start = ctor.position;
1068 } else if arc.end == segment_id {
1069 arc_ctor.end = ctor.position;
1070 } else {
1071 arc_ctor.center = ctor.position;
1072 }
1073 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
1074 let mut arc_ctor = arc_ctor.clone();
1075 if arc.start == segment_id {
1076 arc_ctor.start = ctor.position;
1077 } else if arc.end == segment_id {
1078 arc_ctor.end = ctor.position;
1079 } else {
1080 arc_ctor.center = ctor.position;
1081 }
1082 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
1083 } else {
1084 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1085 "Internal: Arc does not have arc ctor, but found {}",
1086 arc.ctor.human_friendly_kind_with_article()
1087 ))));
1088 }
1089 continue;
1090 }
1091 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
1092 if let Some(existing) = final_edits.get_mut(&owner_id) {
1093 let SegmentCtor::Circle(circle_ctor) = existing else {
1094 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1095 "Internal: Expected circle ctor for owner, but found {}",
1096 existing.human_friendly_kind_with_article()
1097 ))));
1098 };
1099 if circle.start == segment_id {
1100 circle_ctor.start = ctor.position;
1101 } else {
1102 circle_ctor.center = ctor.position;
1103 }
1104 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
1105 let mut circle_ctor = circle_ctor.clone();
1106 if circle.start == segment_id {
1107 circle_ctor.start = ctor.position;
1108 } else {
1109 circle_ctor.center = ctor.position;
1110 }
1111 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
1112 } else {
1113 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1114 "Internal: Circle does not have circle ctor, but found {}",
1115 circle.ctor.human_friendly_kind_with_article()
1116 ))));
1117 }
1118 continue;
1119 }
1120 Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
1121 let Some(control_index) =
1122 spline.controls.iter().position(|control_id| *control_id == segment_id)
1123 else {
1124 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1125 "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
1126 ))));
1127 };
1128 if let Some(existing) = final_edits.get_mut(&owner_id) {
1129 let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
1130 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1131 "Internal: Expected controlPointSpline ctor for owner, but found {}",
1132 existing.human_friendly_kind_with_article()
1133 ))));
1134 };
1135 spline_ctor.points[control_index] = ctor.position;
1136 } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
1137 let mut spline_ctor = spline_ctor.clone();
1138 spline_ctor.points[control_index] = ctor.position;
1139 final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
1140 } else {
1141 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1142 "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1143 spline.ctor.human_friendly_kind_with_article()
1144 ))));
1145 }
1146 continue;
1147 }
1148 _ => {}
1149 }
1150 }
1151
1152 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1154 }
1155 SegmentCtor::Line(ctor) => {
1156 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1157 }
1158 SegmentCtor::Arc(ctor) => {
1159 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1160 }
1161 SegmentCtor::Circle(ctor) => {
1162 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1163 }
1164 SegmentCtor::ControlPointSpline(ctor) => {
1165 final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1166 }
1167 }
1168 }
1169
1170 for (segment_id, ctor) in final_edits {
1171 match ctor {
1172 SegmentCtor::Point(ctor) => self
1173 .edit_point(&mut new_ast, sketch, segment_id, ctor)
1174 .map_err(KclErrorWithOutputs::no_outputs)?,
1175 SegmentCtor::Line(ctor) => self
1176 .edit_line(&mut new_ast, sketch, segment_id, ctor)
1177 .map_err(KclErrorWithOutputs::no_outputs)?,
1178 SegmentCtor::Arc(ctor) => self
1179 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1180 .map_err(KclErrorWithOutputs::no_outputs)?,
1181 SegmentCtor::Circle(ctor) => self
1182 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1183 .map_err(KclErrorWithOutputs::no_outputs)?,
1184 SegmentCtor::ControlPointSpline(ctor) => self
1185 .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1186 .map_err(KclErrorWithOutputs::no_outputs)?,
1187 }
1188 }
1189 for edit in constraint_label_edits {
1190 self.mutate_constraint_label_position(&mut new_ast, edit.constraint_id, edit.label_position)
1191 .map_err(KclErrorWithOutputs::no_outputs)?;
1192 }
1193 let (source_delta, mut scene_graph_delta) = self
1194 .execute_after_edit(
1195 ctx,
1196 sketch,
1197 sketch_block_ref,
1198 &mut new_ast,
1199 ExecuteAfterEditOptions {
1200 segment_ids_edited: drag_anchor_segment_ids,
1201 edit_kind: EditDeleteKind::Edit,
1202 commit_solved_initial_guesses,
1203 },
1204 )
1205 .await?;
1206 if invalidates_ids {
1207 scene_graph_delta.invalidates_ids = true;
1208 }
1209 Ok((source_delta, scene_graph_delta))
1210 }
1211
1212 async fn delete_objects(
1213 &mut self,
1214 ctx: &ExecutorContext,
1215 _version: Version,
1216 sketch: ObjectId,
1217 constraint_ids: Vec<ObjectId>,
1218 segment_ids: Vec<ObjectId>,
1219 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1220 let sketch_block_ref =
1222 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1223
1224 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1226 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1227
1228 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1231
1232 for segment_id in segment_ids_set.iter().copied() {
1233 let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1234 let ObjectKind::Segment { segment } = &segment_object.kind else {
1235 return None;
1236 };
1237 match segment {
1238 Segment::Point(point) => point.owner,
1239 Segment::Line(line) => line.owner,
1240 _ => None,
1241 }
1242 });
1243
1244 if let Some(owner_id) = owner_id
1245 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1246 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1247 && matches!(
1248 owner_segment,
1249 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1250 )
1251 {
1252 resolved_segment_ids_to_delete.insert(owner_id);
1254 } else {
1255 resolved_segment_ids_to_delete.insert(segment_id);
1257 }
1258 }
1259 let referenced_constraint_ids = self
1260 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1261 .map_err(KclErrorWithOutputs::no_outputs)?;
1262
1263 let mut new_ast = self.program.ast.clone();
1264
1265 for constraint_id in referenced_constraint_ids {
1266 if constraint_ids_set.contains(&constraint_id) {
1267 continue;
1268 }
1269
1270 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1271 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1272 })?;
1273 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1274 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1275 "Object is not a constraint, it is {}",
1276 constraint_object.kind.human_friendly_kind_with_article()
1277 ))));
1278 };
1279
1280 match constraint {
1281 Constraint::Coincident(coincident) => {
1282 let remaining_segments =
1283 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1284
1285 if remaining_segments.len() >= 2 {
1287 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1288 .map_err(KclErrorWithOutputs::no_outputs)?;
1289 } else {
1290 constraint_ids_set.insert(constraint_id);
1291 }
1292 }
1293 Constraint::EqualRadius(equal_radius) => {
1294 let remaining_input = equal_radius
1295 .input
1296 .iter()
1297 .copied()
1298 .filter(|segment_id| {
1299 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1300 })
1301 .collect::<Vec<_>>();
1302
1303 if remaining_input.len() >= 2 {
1304 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1305 .map_err(KclErrorWithOutputs::no_outputs)?;
1306 } else {
1307 constraint_ids_set.insert(constraint_id);
1308 }
1309 }
1310 Constraint::LinesEqualLength(lines_equal_length) => {
1311 let remaining_lines = lines_equal_length
1312 .lines
1313 .iter()
1314 .copied()
1315 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1316 .collect::<Vec<_>>();
1317
1318 if remaining_lines.len() >= 2 {
1320 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1321 .map_err(KclErrorWithOutputs::no_outputs)?;
1322 } else {
1323 constraint_ids_set.insert(constraint_id);
1324 }
1325 }
1326 Constraint::Parallel(parallel) => {
1327 let remaining_lines = parallel
1328 .lines
1329 .iter()
1330 .copied()
1331 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1332 .collect::<Vec<_>>();
1333
1334 if remaining_lines.len() >= 2 {
1335 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1336 .map_err(KclErrorWithOutputs::no_outputs)?;
1337 } else {
1338 constraint_ids_set.insert(constraint_id);
1339 }
1340 }
1341 Constraint::Horizontal(Horizontal::Points { points }) => {
1342 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1343
1344 if remaining_points.len() >= 2 {
1345 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1346 .map_err(KclErrorWithOutputs::no_outputs)?;
1347 } else {
1348 constraint_ids_set.insert(constraint_id);
1349 }
1350 }
1351 Constraint::Vertical(Vertical::Points { points }) => {
1352 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1353
1354 if remaining_points.len() >= 2 {
1355 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1356 .map_err(KclErrorWithOutputs::no_outputs)?;
1357 } else {
1358 constraint_ids_set.insert(constraint_id);
1359 }
1360 }
1361 Constraint::Fixed(fixed) => {
1362 if fixed.points.iter().any(|fixed_point| {
1363 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1364 }) {
1365 constraint_ids_set.insert(constraint_id);
1366 }
1367 }
1368 _ => {
1369 constraint_ids_set.insert(constraint_id);
1371 }
1372 }
1373 }
1374
1375 for constraint_id in constraint_ids_set {
1376 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1377 .map_err(KclErrorWithOutputs::no_outputs)?;
1378 }
1379 for segment_id in resolved_segment_ids_to_delete {
1380 self.delete_segment(&mut new_ast, sketch, segment_id)
1381 .map_err(KclErrorWithOutputs::no_outputs)?;
1382 }
1383
1384 self.execute_after_edit(
1385 ctx,
1386 sketch,
1387 sketch_block_ref,
1388 &mut new_ast,
1389 ExecuteAfterEditOptions {
1390 segment_ids_edited: Default::default(),
1391 edit_kind: EditDeleteKind::DeleteNonSketch,
1392 commit_solved_initial_guesses: true,
1393 },
1394 )
1395 .await
1396 }
1397
1398 async fn add_constraint(
1399 &mut self,
1400 ctx: &ExecutorContext,
1401 _version: Version,
1402 sketch: ObjectId,
1403 constraint: Constraint,
1404 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1405 let original_program = self.program.clone();
1409 let original_scene_graph = self.scene_graph.clone();
1410
1411 let mut new_ast = self.program.ast.clone();
1412 let sketch_block_ref = match constraint {
1413 Constraint::Coincident(coincident) => self
1414 .add_coincident(sketch, coincident, &mut new_ast)
1415 .await
1416 .map_err(KclErrorWithOutputs::no_outputs)?,
1417 Constraint::Distance(distance) => self
1418 .add_distance(sketch, distance, &mut new_ast)
1419 .await
1420 .map_err(KclErrorWithOutputs::no_outputs)?,
1421 Constraint::EqualRadius(equal_radius) => self
1422 .add_equal_radius(sketch, equal_radius, &mut new_ast)
1423 .await
1424 .map_err(KclErrorWithOutputs::no_outputs)?,
1425 Constraint::Fixed(fixed) => self
1426 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1427 .await
1428 .map_err(KclErrorWithOutputs::no_outputs)?,
1429 Constraint::HorizontalDistance(distance) => self
1430 .add_horizontal_distance(sketch, distance, &mut new_ast)
1431 .await
1432 .map_err(KclErrorWithOutputs::no_outputs)?,
1433 Constraint::VerticalDistance(distance) => self
1434 .add_vertical_distance(sketch, distance, &mut new_ast)
1435 .await
1436 .map_err(KclErrorWithOutputs::no_outputs)?,
1437 Constraint::Horizontal(horizontal) => self
1438 .add_horizontal(sketch, horizontal, &mut new_ast)
1439 .await
1440 .map_err(KclErrorWithOutputs::no_outputs)?,
1441 Constraint::LinesEqualLength(lines_equal_length) => self
1442 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1443 .await
1444 .map_err(KclErrorWithOutputs::no_outputs)?,
1445 Constraint::Midpoint(midpoint) => self
1446 .add_midpoint(sketch, midpoint, &mut new_ast)
1447 .await
1448 .map_err(KclErrorWithOutputs::no_outputs)?,
1449 Constraint::Parallel(parallel) => self
1450 .add_parallel(sketch, parallel, &mut new_ast)
1451 .await
1452 .map_err(KclErrorWithOutputs::no_outputs)?,
1453 Constraint::Perpendicular(perpendicular) => self
1454 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1455 .await
1456 .map_err(KclErrorWithOutputs::no_outputs)?,
1457 Constraint::Radius(radius) => self
1458 .add_radius(sketch, radius, &mut new_ast)
1459 .await
1460 .map_err(KclErrorWithOutputs::no_outputs)?,
1461 Constraint::Diameter(diameter) => self
1462 .add_diameter(sketch, diameter, &mut new_ast)
1463 .await
1464 .map_err(KclErrorWithOutputs::no_outputs)?,
1465 Constraint::Symmetric(symmetric) => self
1466 .add_symmetric(sketch, symmetric, &mut new_ast)
1467 .await
1468 .map_err(KclErrorWithOutputs::no_outputs)?,
1469 Constraint::Vertical(vertical) => self
1470 .add_vertical(sketch, vertical, &mut new_ast)
1471 .await
1472 .map_err(KclErrorWithOutputs::no_outputs)?,
1473 Constraint::Angle(lines_at_angle) => self
1474 .add_angle(sketch, lines_at_angle, &mut new_ast)
1475 .await
1476 .map_err(KclErrorWithOutputs::no_outputs)?,
1477 Constraint::Tangent(tangent) => self
1478 .add_tangent(sketch, tangent, &mut new_ast)
1479 .await
1480 .map_err(KclErrorWithOutputs::no_outputs)?,
1481 };
1482
1483 let result = self
1484 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1485 .await;
1486
1487 if result.is_err() {
1489 self.program = original_program;
1490 self.scene_graph = original_scene_graph;
1491 }
1492
1493 result
1494 }
1495
1496 async fn chain_segment(
1497 &mut self,
1498 ctx: &ExecutorContext,
1499 version: Version,
1500 sketch: ObjectId,
1501 previous_segment_end_point_id: ObjectId,
1502 segment: SegmentCtor,
1503 _label: Option<String>,
1504 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1505 let SegmentCtor::Line(line_ctor) = segment else {
1509 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1510 "chain_segment currently only supports Line segments, got {}",
1511 segment.human_friendly_kind_with_article(),
1512 ))));
1513 };
1514
1515 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1517
1518 let new_line_id = first_scene_delta
1521 .new_objects
1522 .iter()
1523 .find(|&obj_id| {
1524 let obj = self.scene_graph.objects.get(obj_id.0);
1525 if let Some(obj) = obj {
1526 matches!(
1527 &obj.kind,
1528 ObjectKind::Segment {
1529 segment: Segment::Line(_)
1530 }
1531 )
1532 } else {
1533 false
1534 }
1535 })
1536 .ok_or_else(|| {
1537 KclErrorWithOutputs::no_outputs(KclError::refactor(
1538 "Failed to find new line segment in scene graph".to_string(),
1539 ))
1540 })?;
1541
1542 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1543 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1544 "New line object not found: {new_line_id:?}"
1545 )))
1546 })?;
1547
1548 let ObjectKind::Segment {
1549 segment: new_line_segment,
1550 } = &new_line_obj.kind
1551 else {
1552 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1553 "Object is not a segment: {new_line_obj:?}"
1554 ))));
1555 };
1556
1557 let Segment::Line(new_line) = new_line_segment else {
1558 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1559 "Segment is not a line: {new_line_segment:?}"
1560 ))));
1561 };
1562
1563 let new_line_start_point_id = new_line.start;
1564
1565 let coincident = Coincident {
1567 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1568 };
1569
1570 let (final_src_delta, final_scene_delta) = self
1571 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1572 .await?;
1573
1574 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1577 combined_new_objects.extend(final_scene_delta.new_objects);
1578
1579 let scene_graph_delta = SceneGraphDelta {
1580 new_graph: self.scene_graph_for_ui(),
1581 invalidates_ids: false,
1582 new_objects: combined_new_objects,
1583 exec_outcome: final_scene_delta.exec_outcome,
1584 };
1585
1586 Ok((final_src_delta, scene_graph_delta))
1587 }
1588
1589 async fn edit_constraint_value(
1591 &mut self,
1592 ctx: &ExecutorContext,
1593 _version: Version,
1594 sketch: ObjectId,
1595 constraint_id: ObjectId,
1596 value_expression: String,
1597 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1598 let sketch_block_ref =
1600 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1601
1602 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1603 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1604 })?;
1605 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1606 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1607 "Object is not a constraint: {constraint_id:?}"
1608 ))));
1609 }
1610
1611 let mut new_ast = self.program.ast.clone();
1612
1613 let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1615 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1616 "Invalid constraint value",
1617 &e,
1618 )))
1619 })?;
1620 if !errors.is_empty() {
1621 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1622 format_compilation_issues("Invalid constraint value", &errors),
1623 )));
1624 }
1625 let mut parsed = parsed.ok_or_else(|| {
1626 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1627 })?;
1628 if parsed.ast.body.is_empty() {
1629 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1630 "Empty value expression".to_string(),
1631 )));
1632 }
1633 let first = parsed.ast.body.remove(0);
1634 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1635 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1636 "Value expression must be a simple expression".to_string(),
1637 )));
1638 };
1639
1640 let new_value: ast::BinaryPart = expr_stmt
1641 .inner
1642 .expression
1643 .try_into()
1644 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1645
1646 self.mutate_ast(
1647 &mut new_ast,
1648 constraint_id,
1649 AstMutateCommand::EditConstraintValue { value: new_value },
1650 )
1651 .map_err(KclErrorWithOutputs::no_outputs)?;
1652
1653 self.execute_after_edit(
1654 ctx,
1655 sketch,
1656 sketch_block_ref,
1657 &mut new_ast,
1658 ExecuteAfterEditOptions {
1659 segment_ids_edited: Default::default(),
1660 edit_kind: EditDeleteKind::Edit,
1661 commit_solved_initial_guesses: true,
1662 },
1663 )
1664 .await
1665 }
1666
1667 async fn edit_distance_constraint_label_position(
1668 &mut self,
1669 ctx: &ExecutorContext,
1670 _version: Version,
1671 sketch: ObjectId,
1672 constraint_id: ObjectId,
1673 label_position: Point2d<Number>,
1674 anchor_segment_ids: Vec<ObjectId>,
1675 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1676 let sketch_block_ref =
1678 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1679
1680 let mut new_ast = self.program.ast.clone();
1681 self.mutate_constraint_label_position(&mut new_ast, constraint_id, label_position)
1682 .map_err(KclErrorWithOutputs::no_outputs)?;
1683 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1684
1685 self.execute_after_edit(
1686 ctx,
1687 sketch,
1688 sketch_block_ref,
1689 &mut new_ast,
1690 ExecuteAfterEditOptions {
1691 segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1692 edit_kind: EditDeleteKind::Edit,
1693 commit_solved_initial_guesses,
1694 },
1695 )
1696 .await
1697 }
1698
1699 async fn batch_split_segment_operations(
1707 &mut self,
1708 ctx: &ExecutorContext,
1709 _version: Version,
1710 sketch: ObjectId,
1711 edit_segments: Vec<ExistingSegmentCtor>,
1712 add_constraints: Vec<Constraint>,
1713 delete_constraint_ids: Vec<ObjectId>,
1714 _new_segment_info: sketch::NewSegmentInfo,
1715 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1716 let sketch_block_ref =
1718 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1719
1720 let mut new_ast = self.program.ast.clone();
1721 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1722
1723 for segment in edit_segments {
1725 segment_ids_edited.insert(segment.id);
1726 match segment.ctor {
1727 SegmentCtor::Point(ctor) => self
1728 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1729 .map_err(KclErrorWithOutputs::no_outputs)?,
1730 SegmentCtor::Line(ctor) => self
1731 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1732 .map_err(KclErrorWithOutputs::no_outputs)?,
1733 SegmentCtor::Arc(ctor) => self
1734 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1735 .map_err(KclErrorWithOutputs::no_outputs)?,
1736 SegmentCtor::Circle(ctor) => self
1737 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1738 .map_err(KclErrorWithOutputs::no_outputs)?,
1739 SegmentCtor::ControlPointSpline(ctor) => self
1740 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1741 .map_err(KclErrorWithOutputs::no_outputs)?,
1742 }
1743 }
1744
1745 for constraint in add_constraints {
1747 match constraint {
1748 Constraint::Coincident(coincident) => {
1749 self.add_coincident(sketch, coincident, &mut new_ast)
1750 .await
1751 .map_err(KclErrorWithOutputs::no_outputs)?;
1752 }
1753 Constraint::Distance(distance) => {
1754 self.add_distance(sketch, distance, &mut new_ast)
1755 .await
1756 .map_err(KclErrorWithOutputs::no_outputs)?;
1757 }
1758 Constraint::EqualRadius(equal_radius) => {
1759 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1760 .await
1761 .map_err(KclErrorWithOutputs::no_outputs)?;
1762 }
1763 Constraint::Fixed(fixed) => {
1764 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1765 .await
1766 .map_err(KclErrorWithOutputs::no_outputs)?;
1767 }
1768 Constraint::HorizontalDistance(distance) => {
1769 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1770 .await
1771 .map_err(KclErrorWithOutputs::no_outputs)?;
1772 }
1773 Constraint::VerticalDistance(distance) => {
1774 self.add_vertical_distance(sketch, distance, &mut new_ast)
1775 .await
1776 .map_err(KclErrorWithOutputs::no_outputs)?;
1777 }
1778 Constraint::Horizontal(horizontal) => {
1779 self.add_horizontal(sketch, horizontal, &mut new_ast)
1780 .await
1781 .map_err(KclErrorWithOutputs::no_outputs)?;
1782 }
1783 Constraint::LinesEqualLength(lines_equal_length) => {
1784 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1785 .await
1786 .map_err(KclErrorWithOutputs::no_outputs)?;
1787 }
1788 Constraint::Midpoint(midpoint) => {
1789 self.add_midpoint(sketch, midpoint, &mut new_ast)
1790 .await
1791 .map_err(KclErrorWithOutputs::no_outputs)?;
1792 }
1793 Constraint::Parallel(parallel) => {
1794 self.add_parallel(sketch, parallel, &mut new_ast)
1795 .await
1796 .map_err(KclErrorWithOutputs::no_outputs)?;
1797 }
1798 Constraint::Perpendicular(perpendicular) => {
1799 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1800 .await
1801 .map_err(KclErrorWithOutputs::no_outputs)?;
1802 }
1803 Constraint::Vertical(vertical) => {
1804 self.add_vertical(sketch, vertical, &mut new_ast)
1805 .await
1806 .map_err(KclErrorWithOutputs::no_outputs)?;
1807 }
1808 Constraint::Diameter(diameter) => {
1809 self.add_diameter(sketch, diameter, &mut new_ast)
1810 .await
1811 .map_err(KclErrorWithOutputs::no_outputs)?;
1812 }
1813 Constraint::Radius(radius) => {
1814 self.add_radius(sketch, radius, &mut new_ast)
1815 .await
1816 .map_err(KclErrorWithOutputs::no_outputs)?;
1817 }
1818 Constraint::Symmetric(symmetric) => {
1819 self.add_symmetric(sketch, symmetric, &mut new_ast)
1820 .await
1821 .map_err(KclErrorWithOutputs::no_outputs)?;
1822 }
1823 Constraint::Angle(angle) => {
1824 self.add_angle(sketch, angle, &mut new_ast)
1825 .await
1826 .map_err(KclErrorWithOutputs::no_outputs)?;
1827 }
1828 Constraint::Tangent(tangent) => {
1829 self.add_tangent(sketch, tangent, &mut new_ast)
1830 .await
1831 .map_err(KclErrorWithOutputs::no_outputs)?;
1832 }
1833 }
1834 }
1835
1836 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1838
1839 let has_constraint_deletions = !constraint_ids_set.is_empty();
1840 for constraint_id in constraint_ids_set {
1841 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1842 .map_err(KclErrorWithOutputs::no_outputs)?;
1843 }
1844
1845 let (source_delta, mut scene_graph_delta) = self
1849 .execute_after_edit(
1850 ctx,
1851 sketch,
1852 sketch_block_ref,
1853 &mut new_ast,
1854 ExecuteAfterEditOptions {
1855 segment_ids_edited,
1856 edit_kind: EditDeleteKind::Edit,
1857 commit_solved_initial_guesses: true,
1858 },
1859 )
1860 .await?;
1861
1862 if has_constraint_deletions {
1865 scene_graph_delta.invalidates_ids = true;
1866 }
1867
1868 Ok((source_delta, scene_graph_delta))
1869 }
1870
1871 async fn batch_tail_cut_operations(
1872 &mut self,
1873 ctx: &ExecutorContext,
1874 _version: Version,
1875 sketch: ObjectId,
1876 edit_segments: Vec<ExistingSegmentCtor>,
1877 add_constraints: Vec<Constraint>,
1878 delete_constraint_ids: Vec<ObjectId>,
1879 additional_edited_segment_ids: Vec<ObjectId>,
1880 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1881 let sketch_block_ref =
1882 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1883
1884 let mut new_ast = self.program.ast.clone();
1885 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1886
1887 for segment in edit_segments {
1889 segment_ids_edited.insert(segment.id);
1890 match segment.ctor {
1891 SegmentCtor::Point(ctor) => self
1892 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1893 .map_err(KclErrorWithOutputs::no_outputs)?,
1894 SegmentCtor::Line(ctor) => self
1895 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1896 .map_err(KclErrorWithOutputs::no_outputs)?,
1897 SegmentCtor::Arc(ctor) => self
1898 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1899 .map_err(KclErrorWithOutputs::no_outputs)?,
1900 SegmentCtor::Circle(ctor) => self
1901 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1902 .map_err(KclErrorWithOutputs::no_outputs)?,
1903 SegmentCtor::ControlPointSpline(ctor) => self
1904 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1905 .map_err(KclErrorWithOutputs::no_outputs)?,
1906 }
1907 }
1908
1909 segment_ids_edited.extend(additional_edited_segment_ids);
1910
1911 for constraint in add_constraints {
1913 match constraint {
1914 Constraint::Coincident(coincident) => {
1915 self.add_coincident(sketch, coincident, &mut new_ast)
1916 .await
1917 .map_err(KclErrorWithOutputs::no_outputs)?;
1918 }
1919 other => {
1920 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1921 "unsupported constraint in tail cut batch: {other:?}"
1922 ))));
1923 }
1924 }
1925 }
1926
1927 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1929
1930 let has_constraint_deletions = !constraint_ids_set.is_empty();
1931 for constraint_id in constraint_ids_set {
1932 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1933 .map_err(KclErrorWithOutputs::no_outputs)?;
1934 }
1935
1936 let (source_delta, mut scene_graph_delta) = self
1940 .execute_after_edit(
1941 ctx,
1942 sketch,
1943 sketch_block_ref,
1944 &mut new_ast,
1945 ExecuteAfterEditOptions {
1946 segment_ids_edited,
1947 edit_kind: EditDeleteKind::Edit,
1948 commit_solved_initial_guesses: true,
1949 },
1950 )
1951 .await?;
1952
1953 if has_constraint_deletions {
1956 scene_graph_delta.invalidates_ids = true;
1957 }
1958
1959 Ok((source_delta, scene_graph_delta))
1960 }
1961}
1962
1963impl FrontendState {
1964 pub async fn evaluate_expression(&self, ctx: &ExecutorContext, mut program: Program) -> ExecResult<ExecOutcome> {
1966 program.ast.inner_attrs.clone_from(&self.program.ast.inner_attrs);
1967 ctx.run_mock(&program, &MockConfig::default()).await
1968 }
1969
1970 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1971 self.program = program.clone();
1972
1973 self.point_freedom_cache.clear();
1984 match ctx.run_with_caching(program).await {
1985 Ok(outcome) => {
1986 let outcome = self.update_state_after_exec(outcome, true);
1987 let checkpoint_id = self
1988 .create_sketch_checkpoint(outcome.clone())
1989 .await
1990 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1991 Ok(SetProgramOutcome::Success {
1992 scene_graph: Box::new(self.scene_graph_for_ui()),
1993 exec_outcome: Box::new(outcome),
1994 checkpoint_id: Some(checkpoint_id),
1995 })
1996 }
1997 Err(mut err) => {
1998 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2001 self.update_state_after_exec(outcome, true);
2002 err.scene_graph = Some(self.scene_graph_for_ui());
2003 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
2004 }
2005 }
2006 }
2007
2008 pub async fn engine_execute(
2011 &mut self,
2012 ctx: &ExecutorContext,
2013 program: Program,
2014 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
2015 self.program = program.clone();
2016
2017 self.point_freedom_cache.clear();
2021 match ctx.run_with_caching(program).await {
2022 Ok(outcome) => {
2023 let outcome = self.update_state_after_exec(outcome, true);
2024 Ok(SceneGraphDelta {
2025 new_graph: self.scene_graph_for_ui(),
2026 exec_outcome: outcome,
2027 new_objects: Default::default(),
2029 invalidates_ids: Default::default(),
2031 })
2032 }
2033 Err(mut err) => {
2034 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2036 self.update_state_after_exec(outcome, true);
2037 err.scene_graph = Some(self.scene_graph_for_ui());
2038 Err(err)
2039 }
2040 }
2041 }
2042
2043 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
2044 if matches!(err.error, KclError::EngineHangup { .. }) {
2045 return Err(err);
2049 }
2050
2051 let KclErrorWithOutputs {
2052 error,
2053 mut non_fatal,
2054 variables,
2055 operations,
2056 artifact_graph,
2057 scene_objects,
2058 source_range_to_object,
2059 var_solutions,
2060 refactor_metadata,
2061 filenames,
2062 source_files,
2063 default_planes,
2064 ..
2065 } = err;
2066
2067 non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2068
2069 Ok(ExecOutcome {
2070 variables,
2071 #[cfg(test)]
2072 test_program_memory: Default::default(),
2073 filenames,
2074 operations,
2075 artifact_graph,
2076 scene_objects,
2077 source_range_to_object,
2078 var_solutions,
2079 refactor_metadata,
2080 issues: non_fatal,
2081 source_files,
2082 default_planes,
2083 })
2084 }
2085
2086 async fn add_point(
2087 &mut self,
2088 ctx: &ExecutorContext,
2089 sketch: ObjectId,
2090 ctor: PointCtor,
2091 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2092 let at_ast = to_ast_point2d(&ctor.position)
2094 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2095 let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2096 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2097 unlabeled: None,
2098 arguments: vec![ast::LabeledArg {
2099 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2100 arg: at_ast,
2101 }],
2102 digest: None,
2103 non_code_meta: Default::default(),
2104 })));
2105
2106 let sketch_id = sketch;
2108 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2109 #[cfg(target_arch = "wasm32")]
2110 web_sys::console::error_1(
2111 &format!(
2112 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2113 self.scene_graph.objects
2114 )
2115 .into(),
2116 );
2117 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2118 })?;
2119 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2120 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2121 "Object is not a sketch, it is {}",
2122 sketch_object.kind.human_friendly_kind_with_article(),
2123 ))));
2124 };
2125 let mut new_ast = self.program.ast.clone();
2127 let (sketch_block_ref, _) = self
2128 .mutate_ast(
2129 &mut new_ast,
2130 sketch_id,
2131 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2132 )
2133 .map_err(KclErrorWithOutputs::no_outputs)?;
2134 let new_source = source_from_ast(&new_ast);
2136 let new_program = parse_frontend_mutation_source(
2138 &new_source,
2139 "Error parsing KCL source after adding point",
2140 "No AST produced after adding point",
2141 )?;
2142
2143 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2144 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2145 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2146 )))
2147 })?;
2148
2149 self.program = new_program.clone();
2151
2152 let mut truncated_program = new_program;
2154 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2155 .map_err(KclErrorWithOutputs::no_outputs)?;
2156
2157 let outcome = ctx
2159 .run_mock(
2160 &truncated_program,
2161 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2162 )
2163 .await?;
2164
2165 let new_object_ids = {
2166 let make_err =
2167 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2168 let segment_id = outcome
2169 .source_range_to_object
2170 .get(&point_node_ref.range)
2171 .copied()
2172 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2173 let segment_object = outcome
2174 .scene_objects
2175 .get(segment_id.0)
2176 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2177 let ObjectKind::Segment { segment } = &segment_object.kind else {
2178 return Err(make_err(format!(
2179 "Object is not a segment, it is {}",
2180 segment_object.kind.human_friendly_kind_with_article()
2181 )));
2182 };
2183 let Segment::Point(_) = segment else {
2184 return Err(make_err(format!(
2185 "Segment is not a point, it is {}",
2186 segment.human_friendly_kind_with_article()
2187 )));
2188 };
2189 vec![segment_id]
2190 };
2191 let src_delta = SourceDelta { text: new_source };
2192 let outcome = self.update_state_after_exec(outcome, false);
2194 let scene_graph_delta = SceneGraphDelta {
2195 new_graph: self.scene_graph_for_ui(),
2196 invalidates_ids: false,
2197 new_objects: new_object_ids,
2198 exec_outcome: outcome,
2199 };
2200 Ok((src_delta, scene_graph_delta))
2201 }
2202
2203 async fn add_line(
2204 &mut self,
2205 ctx: &ExecutorContext,
2206 sketch: ObjectId,
2207 ctor: LineCtor,
2208 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2209 let start_ast = to_ast_point2d(&ctor.start)
2211 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2212 let end_ast = to_ast_point2d(&ctor.end)
2213 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2214 let mut arguments = vec![
2215 ast::LabeledArg {
2216 label: Some(ast::Identifier::new(LINE_START_PARAM)),
2217 arg: start_ast,
2218 },
2219 ast::LabeledArg {
2220 label: Some(ast::Identifier::new(LINE_END_PARAM)),
2221 arg: end_ast,
2222 },
2223 ];
2224 if ctor.construction == Some(true) {
2226 arguments.push(ast::LabeledArg {
2227 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2228 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2229 value: ast::LiteralValue::Bool(true),
2230 raw: "true".to_string(),
2231 digest: None,
2232 }))),
2233 });
2234 }
2235 let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2236 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2237 unlabeled: None,
2238 arguments,
2239 digest: None,
2240 non_code_meta: Default::default(),
2241 })));
2242
2243 let sketch_id = sketch;
2245 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2246 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2247 })?;
2248 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2249 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2250 "Object is not a sketch, it is {}",
2251 sketch_object.kind.human_friendly_kind_with_article(),
2252 ))));
2253 };
2254 let mut new_ast = self.program.ast.clone();
2256 let (sketch_block_ref, _) = self
2257 .mutate_ast(
2258 &mut new_ast,
2259 sketch_id,
2260 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2261 )
2262 .map_err(KclErrorWithOutputs::no_outputs)?;
2263 let new_source = source_from_ast(&new_ast);
2265 let new_program = parse_frontend_mutation_source(
2267 &new_source,
2268 "Error parsing KCL source after adding line",
2269 "No AST produced after adding line",
2270 )?;
2271
2272 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2273 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2274 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2275 )))
2276 })?;
2277
2278 self.program = new_program.clone();
2280
2281 let mut truncated_program = new_program;
2283 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2284 .map_err(KclErrorWithOutputs::no_outputs)?;
2285
2286 let outcome = ctx
2288 .run_mock(
2289 &truncated_program,
2290 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2291 )
2292 .await?;
2293
2294 let new_object_ids = {
2295 let make_err =
2296 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2297 let segment_id = outcome
2298 .source_range_to_object
2299 .get(&line_node_ref.range)
2300 .copied()
2301 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2302 let segment_object = outcome
2303 .scene_object_by_id(segment_id)
2304 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2305 let ObjectKind::Segment { segment } = &segment_object.kind else {
2306 return Err(make_err(format!(
2307 "Object is not a segment, it is {}",
2308 segment_object.kind.human_friendly_kind_with_article()
2309 )));
2310 };
2311 let Segment::Line(line) = segment else {
2312 return Err(make_err(format!(
2313 "Segment is not a line, it is {}",
2314 segment.human_friendly_kind_with_article()
2315 )));
2316 };
2317 vec![line.start, line.end, segment_id]
2318 };
2319 let src_delta = SourceDelta { text: new_source };
2320 let outcome = self.update_state_after_exec(outcome, false);
2322 let scene_graph_delta = SceneGraphDelta {
2323 new_graph: self.scene_graph_for_ui(),
2324 invalidates_ids: false,
2325 new_objects: new_object_ids,
2326 exec_outcome: outcome,
2327 };
2328 Ok((src_delta, scene_graph_delta))
2329 }
2330
2331 async fn add_arc(
2332 &mut self,
2333 ctx: &ExecutorContext,
2334 sketch: ObjectId,
2335 ctor: ArcCtor,
2336 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2337 let start_ast = to_ast_point2d(&ctor.start)
2339 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2340 let end_ast = to_ast_point2d(&ctor.end)
2341 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2342 let center_ast = to_ast_point2d(&ctor.center)
2343 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2344 let mut arguments = vec![
2345 ast::LabeledArg {
2346 label: Some(ast::Identifier::new(ARC_START_PARAM)),
2347 arg: start_ast,
2348 },
2349 ast::LabeledArg {
2350 label: Some(ast::Identifier::new(ARC_END_PARAM)),
2351 arg: end_ast,
2352 },
2353 ast::LabeledArg {
2354 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2355 arg: center_ast,
2356 },
2357 ];
2358 if ctor.direction == Some(ArcDirection::Cw) {
2361 arguments.push(ast::LabeledArg {
2362 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2363 arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2364 });
2365 }
2366 if ctor.construction == Some(true) {
2368 arguments.push(ast::LabeledArg {
2369 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2370 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2371 value: ast::LiteralValue::Bool(true),
2372 raw: "true".to_string(),
2373 digest: None,
2374 }))),
2375 });
2376 }
2377 let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2378 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2379 unlabeled: None,
2380 arguments,
2381 digest: None,
2382 non_code_meta: Default::default(),
2383 })));
2384
2385 let sketch_id = sketch;
2387 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2388 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2389 })?;
2390 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2391 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2392 "Object is not a sketch, it is {}",
2393 sketch_object.kind.human_friendly_kind_with_article(),
2394 ))));
2395 };
2396 let mut new_ast = self.program.ast.clone();
2398 let (sketch_block_ref, _) = self
2399 .mutate_ast(
2400 &mut new_ast,
2401 sketch_id,
2402 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2403 )
2404 .map_err(KclErrorWithOutputs::no_outputs)?;
2405 let new_source = source_from_ast(&new_ast);
2407 let new_program = parse_frontend_mutation_source(
2409 &new_source,
2410 "Error parsing KCL source after adding arc",
2411 "No AST produced after adding arc",
2412 )?;
2413
2414 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2415 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2416 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2417 )))
2418 })?;
2419
2420 self.program = new_program.clone();
2422
2423 let mut truncated_program = new_program;
2425 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2426 .map_err(KclErrorWithOutputs::no_outputs)?;
2427
2428 let outcome = ctx
2430 .run_mock(
2431 &truncated_program,
2432 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2433 )
2434 .await?;
2435
2436 let new_object_ids = {
2437 let make_err =
2438 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2439 let segment_id = outcome
2440 .source_range_to_object
2441 .get(&arc_node_ref.range)
2442 .copied()
2443 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2444 let segment_object = outcome
2445 .scene_objects
2446 .get(segment_id.0)
2447 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2448 let ObjectKind::Segment { segment } = &segment_object.kind else {
2449 return Err(make_err(format!(
2450 "Object is not a segment, it is {}",
2451 segment_object.kind.human_friendly_kind_with_article()
2452 )));
2453 };
2454 let Segment::Arc(arc) = segment else {
2455 return Err(make_err(format!(
2456 "Segment is not an arc, it is {}",
2457 segment.human_friendly_kind_with_article()
2458 )));
2459 };
2460 vec![arc.start, arc.end, arc.center, segment_id]
2461 };
2462 let src_delta = SourceDelta { text: new_source };
2463 let outcome = self.update_state_after_exec(outcome, false);
2465 let scene_graph_delta = SceneGraphDelta {
2466 new_graph: self.scene_graph_for_ui(),
2467 invalidates_ids: false,
2468 new_objects: new_object_ids,
2469 exec_outcome: outcome,
2470 };
2471 Ok((src_delta, scene_graph_delta))
2472 }
2473
2474 async fn add_circle(
2475 &mut self,
2476 ctx: &ExecutorContext,
2477 sketch: ObjectId,
2478 ctor: CircleCtor,
2479 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2480 let start_ast = to_ast_point2d(&ctor.start)
2482 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2483 let center_ast = to_ast_point2d(&ctor.center)
2484 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2485 let mut arguments = vec![
2486 ast::LabeledArg {
2487 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2488 arg: start_ast,
2489 },
2490 ast::LabeledArg {
2491 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2492 arg: center_ast,
2493 },
2494 ];
2495 if ctor.construction == Some(true) {
2497 arguments.push(ast::LabeledArg {
2498 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2499 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2500 value: ast::LiteralValue::Bool(true),
2501 raw: "true".to_string(),
2502 digest: None,
2503 }))),
2504 });
2505 }
2506 let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2507 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2508 unlabeled: None,
2509 arguments,
2510 digest: None,
2511 non_code_meta: Default::default(),
2512 })));
2513
2514 let sketch_id = sketch;
2516 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2517 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2518 })?;
2519 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2520 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2521 "Object is not a sketch, it is {}",
2522 sketch_object.kind.human_friendly_kind_with_article(),
2523 ))));
2524 };
2525 let mut new_ast = self.program.ast.clone();
2527 let (sketch_block_ref, _) = self
2528 .mutate_ast(
2529 &mut new_ast,
2530 sketch_id,
2531 AstMutateCommand::AddSketchBlockVarDecl {
2532 prefix: CIRCLE_VARIABLE.to_owned(),
2533 expr: circle_ast,
2534 },
2535 )
2536 .map_err(KclErrorWithOutputs::no_outputs)?;
2537 let new_source = source_from_ast(&new_ast);
2539 let new_program = parse_frontend_mutation_source(
2541 &new_source,
2542 "Error parsing KCL source after adding circle",
2543 "No AST produced after adding circle",
2544 )?;
2545
2546 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2547 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2548 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2549 )))
2550 })?;
2551
2552 self.program = new_program.clone();
2554
2555 let mut truncated_program = new_program;
2557 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2558 .map_err(KclErrorWithOutputs::no_outputs)?;
2559
2560 let outcome = ctx
2562 .run_mock(
2563 &truncated_program,
2564 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2565 )
2566 .await?;
2567
2568 let new_object_ids = {
2569 let make_err =
2570 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2571 let segment_id = outcome
2572 .source_range_to_object
2573 .get(&circle_node_ref.range)
2574 .copied()
2575 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2576 let segment_object = outcome
2577 .scene_objects
2578 .get(segment_id.0)
2579 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2580 let ObjectKind::Segment { segment } = &segment_object.kind else {
2581 return Err(make_err(format!(
2582 "Object is not a segment, it is {}",
2583 segment_object.kind.human_friendly_kind_with_article()
2584 )));
2585 };
2586 let Segment::Circle(circle) = segment else {
2587 return Err(make_err(format!(
2588 "Segment is not a circle, it is {}",
2589 segment.human_friendly_kind_with_article()
2590 )));
2591 };
2592 vec![circle.start, circle.center, segment_id]
2593 };
2594 let src_delta = SourceDelta { text: new_source };
2595 let outcome = self.update_state_after_exec(outcome, false);
2597 let scene_graph_delta = SceneGraphDelta {
2598 new_graph: self.scene_graph_for_ui(),
2599 invalidates_ids: false,
2600 new_objects: new_object_ids,
2601 exec_outcome: outcome,
2602 };
2603 Ok((src_delta, scene_graph_delta))
2604 }
2605
2606 async fn add_control_point_spline(
2607 &mut self,
2608 ctx: &ExecutorContext,
2609 sketch: ObjectId,
2610 ctor: ControlPointSplineCtor,
2611 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2612 let new_program = ensure_control_point_spline_experimental_features(&self.program)
2613 .map_err(KclErrorWithOutputs::no_outputs)?;
2614
2615 let points_ast = to_ast_point2d_array(&ctor.points)
2616 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2617 let mut arguments = vec![ast::LabeledArg {
2618 label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2619 arg: points_ast,
2620 }];
2621 if ctor.construction == Some(true) {
2622 arguments.push(ast::LabeledArg {
2623 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2624 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2625 value: ast::LiteralValue::Bool(true),
2626 raw: "true".to_string(),
2627 digest: None,
2628 }))),
2629 });
2630 }
2631 let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2632 callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2633 unlabeled: None,
2634 arguments,
2635 digest: None,
2636 non_code_meta: Default::default(),
2637 })));
2638
2639 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2640 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2641 })?;
2642 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2643 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2644 "Object is not a sketch, it is {}",
2645 sketch_object.kind.human_friendly_kind_with_article(),
2646 ))));
2647 };
2648
2649 let mut new_ast = new_program.ast.clone();
2650 let (sketch_block_ref, _) = self
2651 .mutate_ast(
2652 &mut new_ast,
2653 sketch,
2654 AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2655 )
2656 .map_err(KclErrorWithOutputs::no_outputs)?;
2657 let new_source = source_from_ast(&new_ast);
2658 let new_program = parse_frontend_mutation_source(
2659 &new_source,
2660 "Error parsing KCL source after adding controlPointSpline",
2661 "No AST produced after adding controlPointSpline",
2662 )?;
2663
2664 let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2665 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2666 "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2667 )))
2668 })?;
2669
2670 self.program = new_program.clone();
2671
2672 let mut truncated_program = new_program;
2673 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2674 .map_err(KclErrorWithOutputs::no_outputs)?;
2675
2676 let outcome = ctx
2677 .run_mock(
2678 &truncated_program,
2679 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2680 )
2681 .await?;
2682
2683 let new_object_ids = {
2684 let make_err =
2685 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2686 let segment_id = outcome
2687 .source_range_to_object
2688 .get(&spline_node_ref.range)
2689 .copied()
2690 .ok_or_else(|| {
2691 make_err(format!(
2692 "Source range of controlPointSpline not found: {spline_node_ref:?}"
2693 ))
2694 })?;
2695 let segment_object = outcome
2696 .scene_objects
2697 .get(segment_id.0)
2698 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2699 let ObjectKind::Segment { segment } = &segment_object.kind else {
2700 return Err(make_err(format!(
2701 "Object is not a segment, it is {}",
2702 segment_object.kind.human_friendly_kind_with_article()
2703 )));
2704 };
2705 let Segment::ControlPointSpline(spline) = segment else {
2706 return Err(make_err(format!(
2707 "Segment is not a control point spline, it is {}",
2708 segment.human_friendly_kind_with_article()
2709 )));
2710 };
2711
2712 let mut ids = outcome
2713 .scene_objects
2714 .iter()
2715 .filter_map(|obj| match &obj.kind {
2716 ObjectKind::Segment {
2717 segment: Segment::Line(line),
2718 } if line.owner == Some(segment_id) => Some(obj.id),
2719 _ => None,
2720 })
2721 .collect::<Vec<_>>();
2722 ids.extend(spline.controls.clone());
2723 ids.push(segment_id);
2724 ids
2725 };
2726 let src_delta = SourceDelta { text: new_source };
2727 let outcome = self.update_state_after_exec(outcome, false);
2728 let scene_graph_delta = SceneGraphDelta {
2729 new_graph: self.scene_graph_for_ui(),
2730 invalidates_ids: false,
2731 new_objects: new_object_ids,
2732 exec_outcome: outcome,
2733 };
2734 Ok((src_delta, scene_graph_delta))
2735 }
2736
2737 fn edit_point(
2738 &mut self,
2739 new_ast: &mut ast::Node<ast::Program>,
2740 sketch: ObjectId,
2741 point: ObjectId,
2742 ctor: PointCtor,
2743 ) -> Result<(), KclError> {
2744 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2746
2747 let sketch_id = sketch;
2749 let sketch_object = self
2750 .scene_graph
2751 .objects
2752 .get(sketch_id.0)
2753 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2754 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2755 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2756 };
2757 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2758 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2759 })?;
2760 let point_id = point;
2762 let point_object = self
2763 .scene_graph
2764 .objects
2765 .get(point_id.0)
2766 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2767 let ObjectKind::Segment {
2768 segment: Segment::Point(point),
2769 } = &point_object.kind
2770 else {
2771 return Err(KclError::refactor(format!(
2772 "Object is not a point segment: {point_object:?}"
2773 )));
2774 };
2775
2776 if let Some(owner_id) = point.owner {
2778 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2779 KclError::refactor(format!(
2780 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2781 ))
2782 })?;
2783 let ObjectKind::Segment { segment } = &owner_object.kind else {
2784 return Err(KclError::refactor(format!(
2785 "Internal: Owner of point is not a segment, but found {}",
2786 owner_object.kind.human_friendly_kind_with_article()
2787 )));
2788 };
2789
2790 if let Segment::Line(line) = segment {
2792 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2793 return Err(KclError::refactor(format!(
2794 "Internal: Owner of point does not have line ctor, but found {}",
2795 line.ctor.human_friendly_kind_with_article()
2796 )));
2797 };
2798 let mut line_ctor = line_ctor.clone();
2799 if line.start == point_id {
2801 line_ctor.start = ctor.position;
2802 } else if line.end == point_id {
2803 line_ctor.end = ctor.position;
2804 } else {
2805 return Err(KclError::refactor(format!(
2806 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2807 )));
2808 }
2809 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2810 }
2811
2812 if let Segment::Arc(arc) = segment {
2814 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2815 return Err(KclError::refactor(format!(
2816 "Internal: Owner of point does not have arc ctor, but found {}",
2817 arc.ctor.human_friendly_kind_with_article()
2818 )));
2819 };
2820 let mut arc_ctor = arc_ctor.clone();
2821 if arc.center == point_id {
2823 arc_ctor.center = ctor.position;
2824 } else if arc.start == point_id {
2825 arc_ctor.start = ctor.position;
2826 } else if arc.end == point_id {
2827 arc_ctor.end = ctor.position;
2828 } else {
2829 return Err(KclError::refactor(format!(
2830 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2831 )));
2832 }
2833 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2834 }
2835
2836 if let Segment::Circle(circle) = segment {
2838 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2839 return Err(KclError::refactor(format!(
2840 "Internal: Owner of point does not have circle ctor, but found {}",
2841 circle.ctor.human_friendly_kind_with_article()
2842 )));
2843 };
2844 let mut circle_ctor = circle_ctor.clone();
2845 if circle.center == point_id {
2846 circle_ctor.center = ctor.position;
2847 } else if circle.start == point_id {
2848 circle_ctor.start = ctor.position;
2849 } else {
2850 return Err(KclError::refactor(format!(
2851 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2852 )));
2853 }
2854 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2855 }
2856
2857 if let Segment::ControlPointSpline(spline) = segment {
2858 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2859 return Err(KclError::refactor(format!(
2860 "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2861 spline.ctor.human_friendly_kind_with_article()
2862 )));
2863 };
2864 let mut spline_ctor = spline_ctor.clone();
2865 let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2866 return Err(KclError::refactor(format!(
2867 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2868 )));
2869 };
2870 spline_ctor.points[control_index] = ctor.position;
2871 return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2872 }
2873
2874 }
2877
2878 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2880 Ok(())
2881 }
2882
2883 fn edit_line(
2884 &mut self,
2885 new_ast: &mut ast::Node<ast::Program>,
2886 sketch: ObjectId,
2887 line: ObjectId,
2888 ctor: LineCtor,
2889 ) -> Result<(), KclError> {
2890 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2892 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2893
2894 let sketch_id = sketch;
2896 let sketch_object = self
2897 .scene_graph
2898 .objects
2899 .get(sketch_id.0)
2900 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2901 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2902 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2903 };
2904 sketch
2905 .segments
2906 .iter()
2907 .find(|o| **o == line)
2908 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2909 let line_id = line;
2911 let line_object = self
2912 .scene_graph
2913 .objects
2914 .get(line_id.0)
2915 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2916 let ObjectKind::Segment { .. } = &line_object.kind else {
2917 let kind = line_object.kind.human_friendly_kind_with_article();
2918 return Err(KclError::refactor(format!(
2919 "This constraint only works on Segments, but you selected {kind}"
2920 )));
2921 };
2922
2923 self.mutate_ast(
2925 new_ast,
2926 line_id,
2927 AstMutateCommand::EditLine {
2928 start: new_start_ast,
2929 end: new_end_ast,
2930 construction: ctor.construction,
2931 },
2932 )?;
2933 Ok(())
2934 }
2935
2936 fn edit_arc(
2937 &mut self,
2938 new_ast: &mut ast::Node<ast::Program>,
2939 sketch: ObjectId,
2940 arc: ObjectId,
2941 ctor: ArcCtor,
2942 ) -> Result<(), KclError> {
2943 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2945 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2946 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2947
2948 let sketch_id = sketch;
2950 let sketch_object = self
2951 .scene_graph
2952 .objects
2953 .get(sketch_id.0)
2954 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2955 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2956 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2957 };
2958 sketch
2959 .segments
2960 .iter()
2961 .find(|o| **o == arc)
2962 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2963 let arc_id = arc;
2965 let arc_object = self
2966 .scene_graph
2967 .objects
2968 .get(arc_id.0)
2969 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2970 let ObjectKind::Segment { .. } = &arc_object.kind else {
2971 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2972 };
2973
2974 self.mutate_ast(
2976 new_ast,
2977 arc_id,
2978 AstMutateCommand::EditArc {
2979 start: new_start_ast,
2980 end: new_end_ast,
2981 center: new_center_ast,
2982 direction: ctor.direction,
2983 construction: ctor.construction,
2984 },
2985 )?;
2986 Ok(())
2987 }
2988
2989 fn edit_circle(
2990 &mut self,
2991 new_ast: &mut ast::Node<ast::Program>,
2992 sketch: ObjectId,
2993 circle: ObjectId,
2994 ctor: CircleCtor,
2995 ) -> Result<(), KclError> {
2996 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2998 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2999
3000 let sketch_id = sketch;
3002 let sketch_object = self
3003 .scene_graph
3004 .objects
3005 .get(sketch_id.0)
3006 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3007 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3008 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3009 };
3010 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3011 KclError::refactor(format!(
3012 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3013 ))
3014 })?;
3015 let circle_id = circle;
3017 let circle_object = self
3018 .scene_graph
3019 .objects
3020 .get(circle_id.0)
3021 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3022 let ObjectKind::Segment { .. } = &circle_object.kind else {
3023 return Err(KclError::refactor(format!(
3024 "Object is not a segment: {circle_object:?}"
3025 )));
3026 };
3027
3028 self.mutate_ast(
3030 new_ast,
3031 circle_id,
3032 AstMutateCommand::EditCircle {
3033 start: new_start_ast,
3034 center: new_center_ast,
3035 construction: ctor.construction,
3036 },
3037 )?;
3038 Ok(())
3039 }
3040
3041 fn edit_control_point_spline(
3042 &mut self,
3043 new_ast: &mut ast::Node<ast::Program>,
3044 sketch: ObjectId,
3045 spline: ObjectId,
3046 ctor: ControlPointSplineCtor,
3047 ) -> Result<(), KclError> {
3048 let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3049
3050 let sketch_object = self
3051 .scene_graph
3052 .objects
3053 .get(sketch.0)
3054 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3055 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3056 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3057 };
3058 sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3059 KclError::refactor(format!(
3060 "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3061 ))
3062 })?;
3063
3064 let spline_object =
3065 self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3066 KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3067 })?;
3068 let ObjectKind::Segment { .. } = &spline_object.kind else {
3069 return Err(KclError::refactor(format!(
3070 "Object is not a segment: {spline_object:?}"
3071 )));
3072 };
3073
3074 self.mutate_ast(
3075 new_ast,
3076 spline,
3077 AstMutateCommand::EditControlPointSpline {
3078 points: points_ast,
3079 construction: ctor.construction,
3080 },
3081 )?;
3082 Ok(())
3083 }
3084
3085 fn delete_segment(
3086 &mut self,
3087 new_ast: &mut ast::Node<ast::Program>,
3088 sketch: ObjectId,
3089 segment_id: ObjectId,
3090 ) -> Result<(), KclError> {
3091 let sketch_id = sketch;
3093 let sketch_object = self
3094 .scene_graph
3095 .objects
3096 .get(sketch_id.0)
3097 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3098 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3099 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3100 };
3101 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3102 KclError::refactor(format!(
3103 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3104 ))
3105 })?;
3106 let segment_object =
3108 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3109 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3110 })?;
3111 let ObjectKind::Segment { .. } = &segment_object.kind else {
3112 return Err(KclError::refactor(format!(
3113 "Object is not a segment, it is {}",
3114 segment_object.kind.human_friendly_kind_with_article()
3115 )));
3116 };
3117
3118 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3120 Ok(())
3121 }
3122
3123 fn delete_constraint(
3124 &mut self,
3125 new_ast: &mut ast::Node<ast::Program>,
3126 sketch: ObjectId,
3127 constraint_id: ObjectId,
3128 ) -> Result<(), KclError> {
3129 let sketch_id = sketch;
3131 let sketch_object = self
3132 .scene_graph
3133 .objects
3134 .get(sketch_id.0)
3135 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3136 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3137 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3138 };
3139 sketch
3140 .constraints
3141 .iter()
3142 .find(|o| **o == constraint_id)
3143 .ok_or_else(|| {
3144 KclError::refactor(format!(
3145 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3146 ))
3147 })?;
3148 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3150 KclError::refactor(format!(
3151 "Constraint not found in scene graph: constraint={constraint_id:?}"
3152 ))
3153 })?;
3154 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3155 return Err(KclError::refactor(format!(
3156 "Object is not a constraint, it is {}",
3157 constraint_object.kind.human_friendly_kind_with_article()
3158 )));
3159 };
3160
3161 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3163 Ok(())
3164 }
3165
3166 fn edit_coincident_constraint(
3167 &mut self,
3168 new_ast: &mut ast::Node<ast::Program>,
3169 constraint_id: ObjectId,
3170 segments: Vec<ConstraintSegment>,
3171 ) -> Result<(), KclError> {
3172 if segments.len() < 2 {
3173 return Err(KclError::refactor(format!(
3174 "Coincident constraint must have at least 2 inputs, got {}",
3175 segments.len()
3176 )));
3177 }
3178
3179 let segment_asts = segments
3180 .iter()
3181 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3182 .collect::<Result<Vec<_>, _>>()?;
3183
3184 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3185 elements: segment_asts,
3186 digest: None,
3187 non_code_meta: Default::default(),
3188 })));
3189
3190 self.mutate_ast(
3191 new_ast,
3192 constraint_id,
3193 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3194 )?;
3195 Ok(())
3196 }
3197
3198 fn edit_horizontal_points_constraint(
3199 &mut self,
3200 new_ast: &mut ast::Node<ast::Program>,
3201 constraint_id: ObjectId,
3202 points: Vec<ConstraintSegment>,
3203 ) -> Result<(), KclError> {
3204 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3205 }
3206
3207 fn edit_vertical_points_constraint(
3208 &mut self,
3209 new_ast: &mut ast::Node<ast::Program>,
3210 constraint_id: ObjectId,
3211 points: Vec<ConstraintSegment>,
3212 ) -> Result<(), KclError> {
3213 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3214 }
3215
3216 fn edit_axis_points_constraint(
3217 &mut self,
3218 new_ast: &mut ast::Node<ast::Program>,
3219 constraint_id: ObjectId,
3220 points: Vec<ConstraintSegment>,
3221 constraint_name: &str,
3222 ) -> Result<(), KclError> {
3223 if points.len() < 2 {
3224 return Err(KclError::refactor(format!(
3225 "{constraint_name} points constraint must have at least 2 points, got {}",
3226 points.len()
3227 )));
3228 }
3229
3230 let point_asts = points
3231 .iter()
3232 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3233 .collect::<Result<Vec<_>, _>>()?;
3234
3235 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3236 elements: point_asts,
3237 digest: None,
3238 non_code_meta: Default::default(),
3239 })));
3240
3241 self.mutate_ast(
3242 new_ast,
3243 constraint_id,
3244 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3245 )?;
3246 Ok(())
3247 }
3248
3249 fn edit_equal_length_constraint(
3251 &mut self,
3252 new_ast: &mut ast::Node<ast::Program>,
3253 constraint_id: ObjectId,
3254 lines: Vec<ObjectId>,
3255 ) -> Result<(), KclError> {
3256 if lines.len() < 2 {
3257 return Err(KclError::refactor(format!(
3258 "Lines equal length constraint must have at least 2 lines, got {}",
3259 lines.len()
3260 )));
3261 }
3262
3263 let line_asts = lines
3264 .iter()
3265 .map(|line_id| {
3266 let line_object = self
3267 .scene_graph
3268 .objects
3269 .get(line_id.0)
3270 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3271 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3272 let kind = line_object.kind.human_friendly_kind_with_article();
3273 return Err(KclError::refactor(format!(
3274 "This constraint only works on Segments, but you selected {kind}"
3275 )));
3276 };
3277 let Segment::Line(_) = line_segment else {
3278 let kind = line_segment.human_friendly_kind_with_article();
3279 return Err(KclError::refactor(format!(
3280 "Only lines can be made equal length, but you selected {kind}"
3281 )));
3282 };
3283
3284 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3285 })
3286 .collect::<Result<Vec<_>, _>>()?;
3287
3288 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3289 elements: line_asts,
3290 digest: None,
3291 non_code_meta: Default::default(),
3292 })));
3293
3294 self.mutate_ast(
3295 new_ast,
3296 constraint_id,
3297 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3298 )?;
3299 Ok(())
3300 }
3301
3302 fn edit_parallel_constraint(
3304 &mut self,
3305 new_ast: &mut ast::Node<ast::Program>,
3306 constraint_id: ObjectId,
3307 lines: Vec<ObjectId>,
3308 ) -> Result<(), KclError> {
3309 if lines.len() < 2 {
3310 return Err(KclError::refactor(format!(
3311 "Parallel constraint must have at least 2 lines, got {}",
3312 lines.len()
3313 )));
3314 }
3315
3316 let line_asts = lines
3317 .iter()
3318 .map(|line_id| {
3319 let line_object = self
3320 .scene_graph
3321 .objects
3322 .get(line_id.0)
3323 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3324 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3325 let kind = line_object.kind.human_friendly_kind_with_article();
3326 return Err(KclError::refactor(format!(
3327 "This constraint only works on Segments, but you selected {kind}"
3328 )));
3329 };
3330 let Segment::Line(_) = line_segment else {
3331 let kind = line_segment.human_friendly_kind_with_article();
3332 return Err(KclError::refactor(format!(
3333 "Only lines can be made parallel, but you selected {kind}"
3334 )));
3335 };
3336
3337 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3338 })
3339 .collect::<Result<Vec<_>, _>>()?;
3340
3341 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3342 elements: line_asts,
3343 digest: None,
3344 non_code_meta: Default::default(),
3345 })));
3346
3347 self.mutate_ast(
3348 new_ast,
3349 constraint_id,
3350 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3351 )?;
3352 Ok(())
3353 }
3354
3355 fn edit_equal_radius_constraint(
3357 &mut self,
3358 new_ast: &mut ast::Node<ast::Program>,
3359 constraint_id: ObjectId,
3360 input: Vec<ObjectId>,
3361 ) -> Result<(), KclError> {
3362 if input.len() < 2 {
3363 return Err(KclError::refactor(format!(
3364 "equalRadius constraint must have at least 2 segments, got {}",
3365 input.len()
3366 )));
3367 }
3368
3369 let input_asts = input
3370 .iter()
3371 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3372 .collect::<Result<Vec<_>, _>>()?;
3373
3374 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3375 elements: input_asts,
3376 digest: None,
3377 non_code_meta: Default::default(),
3378 })));
3379
3380 self.mutate_ast(
3381 new_ast,
3382 constraint_id,
3383 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3384 )?;
3385 Ok(())
3386 }
3387
3388 async fn execute_after_edit(
3389 &mut self,
3390 ctx: &ExecutorContext,
3391 sketch: ObjectId,
3392 sketch_block_ref: AstNodeRef,
3393 new_ast: &mut ast::Node<ast::Program>,
3394 options: ExecuteAfterEditOptions,
3395 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3396 let ExecuteAfterEditOptions {
3397 segment_ids_edited,
3398 edit_kind,
3399 commit_solved_initial_guesses,
3400 } = options;
3401
3402 let new_source = source_from_ast(new_ast);
3404 let new_program = parse_frontend_mutation_source(
3406 &new_source,
3407 "Error parsing KCL source after editing",
3408 "No AST produced after editing",
3409 )?;
3410
3411 let is_delete = edit_kind.is_delete();
3413 let truncated_program = {
3414 let mut truncated_program = new_program.clone();
3415 only_sketch_block(
3416 &mut truncated_program.ast,
3417 &sketch_block_ref,
3418 edit_kind.to_change_kind(),
3419 )
3420 .map_err(KclErrorWithOutputs::no_outputs)?;
3421 truncated_program
3422 };
3423
3424 let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3426 let mock_config = MockConfig {
3427 sketch_block_id: Some(sketch),
3428 freedom_analysis: is_delete,
3429 segment_ids_edited: segment_ids_edited.clone(),
3430 drag_anchors,
3431 ..Default::default()
3432 };
3433 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3434
3435 self.program = new_program;
3437
3438 let outcome = self.update_state_after_exec(outcome, is_delete);
3440
3441 let src_delta = if commit_solved_initial_guesses {
3442 self.commit_var_solutions_to_program(&outcome, "editing")?
3443 } else {
3444 SourceDelta { text: new_source }
3445 };
3446 let scene_graph_delta = SceneGraphDelta {
3447 new_graph: self.scene_graph_for_ui(),
3448 invalidates_ids: is_delete,
3449 new_objects: Vec::new(),
3450 exec_outcome: outcome,
3451 };
3452 Ok((src_delta, scene_graph_delta))
3453 }
3454
3455 async fn execute_after_delete_sketch(
3456 &mut self,
3457 ctx: &ExecutorContext,
3458 new_ast: &mut ast::Node<ast::Program>,
3459 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3460 let new_source = source_from_ast(new_ast);
3462 let new_program = parse_frontend_mutation_source(
3464 &new_source,
3465 "Error parsing KCL source after editing",
3466 "No AST produced after editing",
3467 )?;
3468
3469 self.program = new_program.clone();
3471
3472 let outcome = ctx.run_with_caching(new_program).await?;
3478 let freedom_analysis_ran = true;
3479
3480 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3481
3482 let src_delta = SourceDelta { text: new_source };
3483 let scene_graph_delta = SceneGraphDelta {
3484 new_graph: self.scene_graph_for_ui(),
3485 invalidates_ids: true,
3486 new_objects: Vec::new(),
3487 exec_outcome: outcome,
3488 };
3489 Ok((src_delta, scene_graph_delta))
3490 }
3491
3492 fn point_id_to_ast_reference(
3497 &self,
3498 point_id: ObjectId,
3499 new_ast: &mut ast::Node<ast::Program>,
3500 ) -> Result<ast::Expr, KclError> {
3501 let point_object = self
3502 .scene_graph
3503 .objects
3504 .get(point_id.0)
3505 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3506 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3507 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3508 };
3509 let Segment::Point(point) = point_segment else {
3510 return Err(KclError::refactor(format!(
3511 "Only points are currently supported: {point_object:?}"
3512 )));
3513 };
3514
3515 if let Some(owner_id) = point.owner {
3516 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3517 KclError::refactor(format!(
3518 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3519 ))
3520 })?;
3521 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3522 return Err(KclError::refactor(format!(
3523 "Owner of point is not a segment, but found {}",
3524 owner_object.kind.human_friendly_kind_with_article()
3525 )));
3526 };
3527
3528 match owner_segment {
3529 Segment::Line(line) => {
3530 let property = if line.start == point_id {
3531 LINE_PROPERTY_START
3532 } else if line.end == point_id {
3533 LINE_PROPERTY_END
3534 } else {
3535 return Err(KclError::refactor(format!(
3536 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3537 )));
3538 };
3539 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3540 }
3541 Segment::Arc(arc) => {
3542 let property = if arc.start == point_id {
3543 ARC_PROPERTY_START
3544 } else if arc.end == point_id {
3545 ARC_PROPERTY_END
3546 } else if arc.center == point_id {
3547 ARC_PROPERTY_CENTER
3548 } else {
3549 return Err(KclError::refactor(format!(
3550 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3551 )));
3552 };
3553 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3554 }
3555 Segment::Circle(circle) => {
3556 let property = if circle.start == point_id {
3557 CIRCLE_PROPERTY_START
3558 } else if circle.center == point_id {
3559 CIRCLE_PROPERTY_CENTER
3560 } else {
3561 return Err(KclError::refactor(format!(
3562 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3563 )));
3564 };
3565 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3566 }
3567 Segment::ControlPointSpline(spline) => {
3568 let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3569 return Err(KclError::refactor(format!(
3570 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3571 )));
3572 };
3573 let owner_expr =
3574 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3575 let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3576 Ok(create_index_expression(controls_expr, index))
3577 }
3578 _ => Err(KclError::refactor(format!(
3579 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3580 ))),
3581 }
3582 } else {
3583 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3585 }
3586 }
3587
3588 fn line_id_to_ast_reference(
3589 &self,
3590 line_id: ObjectId,
3591 new_ast: &mut ast::Node<ast::Program>,
3592 ) -> Result<ast::Expr, KclError> {
3593 let line_object = self
3594 .scene_graph
3595 .objects
3596 .get(line_id.0)
3597 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3598 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3599 return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3600 };
3601 let Segment::Line(line) = line_segment else {
3602 return Err(KclError::refactor(format!(
3603 "Only lines are currently supported: {line_object:?}"
3604 )));
3605 };
3606
3607 if let Some(owner_id) = line.owner {
3608 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3609 KclError::refactor(format!(
3610 "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3611 ))
3612 })?;
3613 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3614 return Err(KclError::refactor(format!(
3615 "Owner of line is not a segment, but found {}",
3616 owner_object.kind.human_friendly_kind_with_article()
3617 )));
3618 };
3619
3620 match owner_segment {
3621 Segment::ControlPointSpline(spline) => {
3622 let Some(index) = spline
3623 .controls
3624 .windows(2)
3625 .position(|window| window[0] == line.start && window[1] == line.end)
3626 else {
3627 return Err(KclError::refactor(format!(
3628 "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3629 )));
3630 };
3631 let owner_expr =
3632 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3633 let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3634 Ok(create_index_expression(edges_expr, index))
3635 }
3636 _ => Err(KclError::refactor(format!(
3637 "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3638 ))),
3639 }
3640 } else {
3641 get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3642 }
3643 }
3644
3645 fn coincident_segment_to_ast(
3646 &self,
3647 segment: &ConstraintSegment,
3648 new_ast: &mut ast::Node<ast::Program>,
3649 ) -> Result<ast::Expr, KclError> {
3650 match segment {
3651 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3652 ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3653 }
3654 }
3655
3656 fn segment_id_to_constraint_ast_reference(
3657 &self,
3658 segment_id: ObjectId,
3659 new_ast: &mut ast::Node<ast::Program>,
3660 ) -> Result<ast::Expr, KclError> {
3661 let segment_object = self
3662 .scene_graph
3663 .objects
3664 .get(segment_id.0)
3665 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3666 let ObjectKind::Segment { segment } = &segment_object.kind else {
3667 return Err(KclError::refactor(format!(
3668 "Object is not a segment, it is {}",
3669 segment_object.kind.human_friendly_kind_with_article()
3670 )));
3671 };
3672
3673 match segment {
3674 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3675 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3676 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3677 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3678 Segment::ControlPointSpline(_) => {
3679 get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3680 }
3681 }
3682 }
3683
3684 fn axis_constraint_segment_to_ast(
3685 &self,
3686 segment: &ConstraintSegment,
3687 new_ast: &mut ast::Node<ast::Program>,
3688 ) -> Result<ast::Expr, KclError> {
3689 match segment {
3690 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3691 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3692 }
3693 }
3694
3695 async fn add_coincident(
3696 &mut self,
3697 sketch: ObjectId,
3698 coincident: Coincident,
3699 new_ast: &mut ast::Node<ast::Program>,
3700 ) -> Result<AstNodeRef, KclError> {
3701 let sketch_id = sketch;
3702 for segment in &coincident.segments {
3703 let ConstraintSegment::Segment(segment_id) = segment else {
3704 continue;
3705 };
3706 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3707 continue;
3708 };
3709 if matches!(
3710 segment_object.kind,
3711 ObjectKind::Segment {
3712 segment: Segment::ControlPointSpline(_)
3713 }
3714 ) {
3715 return Err(KclError::refactor(
3716 "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3717 .to_owned(),
3718 ));
3719 }
3720 }
3721 let segment_asts = coincident
3722 .segments
3723 .iter()
3724 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3725 .collect::<Result<Vec<_>, _>>()?;
3726 if segment_asts.len() < 2 {
3727 return Err(KclError::refactor(format!(
3728 "Coincident constraint must have at least 2 inputs, got {}",
3729 segment_asts.len()
3730 )));
3731 }
3732
3733 let coincident_ast = create_coincident_ast(segment_asts);
3735
3736 let (sketch_block_ref, _) = self.mutate_ast(
3738 new_ast,
3739 sketch_id,
3740 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3741 )?;
3742 Ok(sketch_block_ref)
3743 }
3744
3745 async fn add_distance(
3746 &mut self,
3747 sketch: ObjectId,
3748 distance: Distance,
3749 new_ast: &mut ast::Node<ast::Program>,
3750 ) -> Result<AstNodeRef, KclError> {
3751 self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3752 }
3753
3754 fn distance_constraint_ast_parts(
3755 &self,
3756 function_name: &str,
3757 distance: &Distance,
3758 new_ast: &mut ast::Node<ast::Program>,
3759 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3760 let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3761 [pt0, pt1] => [
3762 self.coincident_segment_to_ast(pt0, new_ast)?,
3763 self.coincident_segment_to_ast(pt1, new_ast)?,
3764 ],
3765 _ => {
3766 return Err(KclError::refactor(format!(
3767 "Distance constraint must have exactly 2 segments, got {}",
3768 distance.segments.len()
3769 )));
3770 }
3771 };
3772
3773 let arguments = match &distance.label_position {
3774 Some(label_position) => vec![ast::LabeledArg {
3775 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3776 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3777 }],
3778 None => Default::default(),
3779 };
3780
3781 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3782 callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3783 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3784 ast::ArrayExpression {
3785 elements: vec![segment0_ast, segment1_ast],
3786 digest: None,
3787 non_code_meta: Default::default(),
3788 },
3789 )))),
3790 arguments,
3791 digest: None,
3792 non_code_meta: Default::default(),
3793 })));
3794 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3795 value: ast::LiteralValue::Number {
3796 value: distance.distance.value,
3797 suffix: distance.distance.units,
3798 },
3799 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3800 KclError::refactor(format!(
3801 "Could not format numeric suffix: {:?}",
3802 distance.distance.units
3803 ))
3804 })?,
3805 digest: None,
3806 })));
3807
3808 Ok((call, value))
3809 }
3810
3811 fn add_distance_constraint(
3812 &mut self,
3813 sketch: ObjectId,
3814 function_name: &str,
3815 distance: Distance,
3816 new_ast: &mut ast::Node<ast::Program>,
3817 ) -> Result<AstNodeRef, KclError> {
3818 let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3819 let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3820 left: call,
3821 operator: ast::BinaryOperator::Eq,
3822 right: value,
3823 digest: None,
3824 })));
3825
3826 let (sketch_block_ref, _) = self.mutate_ast(
3827 new_ast,
3828 sketch,
3829 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3830 )?;
3831 Ok(sketch_block_ref)
3832 }
3833
3834 async fn add_angle(
3835 &mut self,
3836 sketch: ObjectId,
3837 angle: Angle,
3838 new_ast: &mut ast::Node<ast::Program>,
3839 ) -> Result<AstNodeRef, KclError> {
3840 let sketch_id = sketch;
3841 let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3842 let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3843 left: angle_call_ast,
3844 operator: ast::BinaryOperator::Eq,
3845 right: angle_value_ast,
3846 digest: None,
3847 })));
3848
3849 let (sketch_block_ref, _) = self.mutate_ast(
3851 new_ast,
3852 sketch_id,
3853 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3854 )?;
3855 Ok(sketch_block_ref)
3856 }
3857
3858 fn angle_constraint_ast_parts(
3859 &self,
3860 angle: &Angle,
3861 new_ast: &mut ast::Node<ast::Program>,
3862 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3863 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3864 return Err(KclError::refactor(format!(
3865 "Angle constraint must have exactly 2 lines, got {}",
3866 angle.lines.len()
3867 )));
3868 };
3869
3870 let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3871 let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3872 let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3873 elements: vec![l0_ast, l1_ast],
3874 digest: None,
3875 non_code_meta: Default::default(),
3876 })));
3877
3878 if angle.inverse == Some(true) && angle.sector.is_none() {
3879 return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3880 }
3881
3882 let uses_angle_dimension = angle.sector.is_some();
3883 let mut arguments = if uses_angle_dimension {
3884 vec![ast::LabeledArg {
3885 label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3886 arg: lines_ast.clone(),
3887 }]
3888 } else {
3889 Default::default()
3890 };
3891
3892 if let Some(sector) = angle.sector {
3893 arguments.push(ast::LabeledArg {
3894 label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3895 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3896 value: ast::LiteralValue::Number {
3897 value: f64::from(sector),
3898 suffix: NumericSuffix::None,
3899 },
3900 raw: sector.to_string(),
3901 digest: None,
3902 }))),
3903 });
3904 }
3905
3906 if angle.inverse == Some(true) {
3907 arguments.push(ast::LabeledArg {
3908 label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3909 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3910 value: ast::LiteralValue::Bool(true),
3911 raw: true.to_string(),
3912 digest: None,
3913 }))),
3914 });
3915 }
3916
3917 if let Some(label_position) = &angle.label_position {
3918 arguments.push(ast::LabeledArg {
3919 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3920 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3921 });
3922 }
3923
3924 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3925 callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3926 ANGLE_DIMENSION_FN
3927 } else {
3928 ANGLE_FN
3929 })),
3930 unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3931 arguments,
3932 digest: None,
3933 non_code_meta: Default::default(),
3934 })));
3935 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3936 value: ast::LiteralValue::Number {
3937 value: angle.angle.value,
3938 suffix: angle.angle.units,
3939 },
3940 raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3941 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3942 digest: None,
3943 })));
3944
3945 Ok((call, value))
3946 }
3947
3948 async fn add_tangent(
3949 &mut self,
3950 sketch: ObjectId,
3951 tangent: Tangent,
3952 new_ast: &mut ast::Node<ast::Program>,
3953 ) -> Result<AstNodeRef, KclError> {
3954 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3955 return Err(KclError::refactor(format!(
3956 "Tangent constraint must have exactly 2 segments, got {}",
3957 tangent.input.len()
3958 )));
3959 };
3960 let sketch_id = sketch;
3961
3962 let seg0_object = self
3963 .scene_graph
3964 .objects
3965 .get(seg0_id.0)
3966 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3967 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3968 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3969 };
3970 let seg0_ast = match seg0_segment {
3971 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3972 self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3973 }
3974 _ => {
3975 return Err(KclError::refactor(format!(
3976 "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3977 )));
3978 }
3979 };
3980
3981 let seg1_object = self
3982 .scene_graph
3983 .objects
3984 .get(seg1_id.0)
3985 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3986 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3987 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3988 };
3989 let seg1_ast = match seg1_segment {
3990 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3991 self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3992 }
3993 _ => {
3994 return Err(KclError::refactor(format!(
3995 "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3996 )));
3997 }
3998 };
3999
4000 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
4001 let (sketch_block_ref, _) = self.mutate_ast(
4002 new_ast,
4003 sketch_id,
4004 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
4005 )?;
4006 Ok(sketch_block_ref)
4007 }
4008
4009 async fn add_symmetric(
4010 &mut self,
4011 sketch: ObjectId,
4012 symmetric: Symmetric,
4013 new_ast: &mut ast::Node<ast::Program>,
4014 ) -> Result<AstNodeRef, KclError> {
4015 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4016 return Err(KclError::refactor(format!(
4017 "Symmetric constraint must have exactly 2 inputs, got {}",
4018 symmetric.input.len()
4019 )));
4020 };
4021 let sketch_id = sketch;
4022
4023 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4024 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4025 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4026
4027 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4028 let (sketch_block_ref, _) = self.mutate_ast(
4029 new_ast,
4030 sketch_id,
4031 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4032 )?;
4033 Ok(sketch_block_ref)
4034 }
4035
4036 async fn add_midpoint(
4037 &mut self,
4038 sketch: ObjectId,
4039 midpoint: Midpoint,
4040 new_ast: &mut ast::Node<ast::Program>,
4041 ) -> Result<AstNodeRef, KclError> {
4042 let sketch_id = sketch;
4043 let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4044
4045 let segment_object = self
4046 .scene_graph
4047 .objects
4048 .get(midpoint.segment.0)
4049 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4050 let ObjectKind::Segment {
4051 segment: midpoint_segment,
4052 } = &segment_object.kind
4053 else {
4054 return Err(KclError::refactor(format!(
4055 "Object must be a segment, but it was {}",
4056 segment_object.kind.human_friendly_kind_with_article()
4057 )));
4058 };
4059 let segment_ast = match midpoint_segment {
4060 Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4061 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4062 _ => {
4063 return Err(KclError::refactor(format!(
4064 "Midpoint target must be a line or arc segment but it was {}",
4065 midpoint_segment.human_friendly_kind_with_article()
4066 )));
4067 }
4068 };
4069
4070 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4071 let (sketch_block_ref, _) = self.mutate_ast(
4072 new_ast,
4073 sketch_id,
4074 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4075 )?;
4076 Ok(sketch_block_ref)
4077 }
4078
4079 async fn add_equal_radius(
4080 &mut self,
4081 sketch: ObjectId,
4082 equal_radius: EqualRadius,
4083 new_ast: &mut ast::Node<ast::Program>,
4084 ) -> Result<AstNodeRef, KclError> {
4085 if equal_radius.input.len() < 2 {
4086 return Err(KclError::refactor(format!(
4087 "equalRadius constraint must have at least 2 segments, got {}",
4088 equal_radius.input.len()
4089 )));
4090 }
4091
4092 let sketch_id = sketch;
4093 let input_asts = equal_radius
4094 .input
4095 .iter()
4096 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4097 .collect::<Result<Vec<_>, _>>()?;
4098
4099 let equal_radius_ast = create_equal_radius_ast(input_asts);
4100 let (sketch_block_ref, _) = self.mutate_ast(
4101 new_ast,
4102 sketch_id,
4103 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4104 )?;
4105 Ok(sketch_block_ref)
4106 }
4107
4108 async fn add_radius(
4109 &mut self,
4110 sketch: ObjectId,
4111 radius: Radius,
4112 new_ast: &mut ast::Node<ast::Program>,
4113 ) -> Result<AstNodeRef, KclError> {
4114 let params = ArcSizeConstraintParams {
4115 points: vec![radius.arc],
4116 function_name: RADIUS_FN,
4117 value: radius.radius.value,
4118 units: radius.radius.units,
4119 label_position: radius.label_position,
4120 constraint_type_name: "Radius",
4121 };
4122 self.add_arc_size_constraint(sketch, params, new_ast).await
4123 }
4124
4125 async fn add_diameter(
4126 &mut self,
4127 sketch: ObjectId,
4128 diameter: Diameter,
4129 new_ast: &mut ast::Node<ast::Program>,
4130 ) -> Result<AstNodeRef, KclError> {
4131 let params = ArcSizeConstraintParams {
4132 points: vec![diameter.arc],
4133 function_name: DIAMETER_FN,
4134 value: diameter.diameter.value,
4135 units: diameter.diameter.units,
4136 label_position: diameter.label_position,
4137 constraint_type_name: "Diameter",
4138 };
4139 self.add_arc_size_constraint(sketch, params, new_ast).await
4140 }
4141
4142 async fn add_fixed_constraints(
4143 &mut self,
4144 sketch: ObjectId,
4145 points: Vec<FixedPoint>,
4146 new_ast: &mut ast::Node<ast::Program>,
4147 ) -> Result<AstNodeRef, KclError> {
4148 let mut sketch_block_ref = None;
4149
4150 for fixed_point in points {
4151 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4152 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4153 .map_err(|err| KclError::refactor(err.to_string()))?;
4154
4155 let (sketch_ref, _) = self.mutate_ast(
4156 new_ast,
4157 sketch,
4158 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4159 )?;
4160 sketch_block_ref = Some(sketch_ref);
4161 }
4162
4163 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4164 }
4165
4166 async fn add_arc_size_constraint(
4167 &mut self,
4168 sketch: ObjectId,
4169 params: ArcSizeConstraintParams,
4170 new_ast: &mut ast::Node<ast::Program>,
4171 ) -> Result<AstNodeRef, KclError> {
4172 let sketch_id = sketch;
4173
4174 if params.points.len() != 1 {
4176 return Err(KclError::refactor(format!(
4177 "{} constraint must have exactly 1 argument (an arc segment), got {}",
4178 params.constraint_type_name,
4179 params.points.len()
4180 )));
4181 }
4182
4183 let arc_id = params.points[0];
4184 let arc_object = self
4185 .scene_graph
4186 .objects
4187 .get(arc_id.0)
4188 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4189 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4190 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4191 };
4192 let ref_type = match arc_segment {
4193 Segment::Arc(_) => ARC_VARIABLE,
4194 Segment::Circle(_) => CIRCLE_VARIABLE,
4195 _ => {
4196 return Err(KclError::refactor(format!(
4197 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4198 params.constraint_type_name
4199 )));
4200 }
4201 };
4202 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4204 let arguments = match ¶ms.label_position {
4205 Some(label_position) => vec![ast::LabeledArg {
4206 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4207 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4208 }],
4209 None => Default::default(),
4210 };
4211
4212 let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4214 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4215 unlabeled: Some(arc_ast),
4216 arguments,
4217 digest: None,
4218 non_code_meta: Default::default(),
4219 })));
4220 let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4221 left: call_ast,
4222 operator: ast::BinaryOperator::Eq,
4223 right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4224 value: ast::LiteralValue::Number {
4225 value: params.value,
4226 suffix: params.units,
4227 },
4228 raw: format_number_literal(params.value, params.units, None)
4229 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4230 digest: None,
4231 }))),
4232 digest: None,
4233 })));
4234
4235 let (sketch_block_ref, _) = self.mutate_ast(
4237 new_ast,
4238 sketch_id,
4239 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4240 )?;
4241 Ok(sketch_block_ref)
4242 }
4243
4244 async fn add_horizontal_distance(
4245 &mut self,
4246 sketch: ObjectId,
4247 distance: Distance,
4248 new_ast: &mut ast::Node<ast::Program>,
4249 ) -> Result<AstNodeRef, KclError> {
4250 self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4251 }
4252
4253 async fn add_vertical_distance(
4254 &mut self,
4255 sketch: ObjectId,
4256 distance: Distance,
4257 new_ast: &mut ast::Node<ast::Program>,
4258 ) -> Result<AstNodeRef, KclError> {
4259 self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4260 }
4261
4262 async fn add_horizontal(
4263 &mut self,
4264 sketch: ObjectId,
4265 horizontal: Horizontal,
4266 new_ast: &mut ast::Node<ast::Program>,
4267 ) -> Result<AstNodeRef, KclError> {
4268 let sketch_id = sketch;
4269
4270 let first_arg_ast = match horizontal {
4272 Horizontal::Line { line } => {
4273 let line_object = self
4274 .scene_graph
4275 .objects
4276 .get(line.0)
4277 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4278 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4279 let kind = line_object.kind.human_friendly_kind_with_article();
4280 return Err(KclError::refactor(format!(
4281 "This constraint only works on Segments, but you selected {kind}"
4282 )));
4283 };
4284 let Segment::Line(_) = line_segment else {
4285 return Err(KclError::refactor(format!(
4286 "Only lines can be made horizontal, but you selected {}",
4287 line_segment.human_friendly_kind_with_article(),
4288 )));
4289 };
4290 self.line_id_to_ast_reference(line, new_ast)?
4291 }
4292 Horizontal::Points { points } => {
4293 let point_asts = points
4294 .iter()
4295 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4296 .collect::<Result<Vec<_>, _>>()?;
4297 ast::ArrayExpression::new(point_asts).into()
4298 }
4299 };
4300 let horizontal_ast = create_horizontal_ast(first_arg_ast);
4302
4303 let (sketch_block_ref, _) = self.mutate_ast(
4305 new_ast,
4306 sketch_id,
4307 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4308 )?;
4309 Ok(sketch_block_ref)
4310 }
4311
4312 async fn add_lines_equal_length(
4313 &mut self,
4314 sketch: ObjectId,
4315 lines_equal_length: LinesEqualLength,
4316 new_ast: &mut ast::Node<ast::Program>,
4317 ) -> Result<AstNodeRef, KclError> {
4318 if lines_equal_length.lines.len() < 2 {
4319 return Err(KclError::refactor(format!(
4320 "Lines equal length constraint must have at least 2 lines, got {}",
4321 lines_equal_length.lines.len()
4322 )));
4323 };
4324
4325 let sketch_id = sketch;
4326
4327 let line_asts = lines_equal_length
4329 .lines
4330 .iter()
4331 .map(|line_id| {
4332 let line_object = self
4333 .scene_graph
4334 .objects
4335 .get(line_id.0)
4336 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4337 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4338 let kind = line_object.kind.human_friendly_kind_with_article();
4339 return Err(KclError::refactor(format!(
4340 "This constraint only works on Segments, but you selected {kind}"
4341 )));
4342 };
4343 let Segment::Line(_) = line_segment else {
4344 let kind = line_segment.human_friendly_kind_with_article();
4345 return Err(KclError::refactor(format!(
4346 "Only lines can be made equal length, but you selected {kind}"
4347 )));
4348 };
4349
4350 self.line_id_to_ast_reference(*line_id, new_ast)
4351 })
4352 .collect::<Result<Vec<_>, _>>()?;
4353
4354 let equal_length_ast = create_equal_length_ast(line_asts);
4356
4357 let (sketch_block_ref, _) = self.mutate_ast(
4359 new_ast,
4360 sketch_id,
4361 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4362 )?;
4363 Ok(sketch_block_ref)
4364 }
4365
4366 fn equal_radius_segment_id_to_ast_reference(
4367 &mut self,
4368 segment_id: ObjectId,
4369 new_ast: &mut ast::Node<ast::Program>,
4370 ) -> Result<ast::Expr, KclError> {
4371 let segment_object = self
4372 .scene_graph
4373 .objects
4374 .get(segment_id.0)
4375 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4376 let ObjectKind::Segment { segment } = &segment_object.kind else {
4377 return Err(KclError::refactor(format!(
4378 "Object is not a segment, it was {}",
4379 segment_object.kind.human_friendly_kind_with_article()
4380 )));
4381 };
4382
4383 let ref_type = match segment {
4384 Segment::Arc(_) => ARC_VARIABLE,
4385 Segment::Circle(_) => CIRCLE_VARIABLE,
4386 _ => {
4387 return Err(KclError::refactor(format!(
4388 "equalRadius supports only arc/circle segments, got {}",
4389 segment.human_friendly_kind_with_article()
4390 )));
4391 }
4392 };
4393
4394 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4395 }
4396
4397 fn symmetric_input_id_to_ast_reference(
4398 &mut self,
4399 segment_id: ObjectId,
4400 new_ast: &mut ast::Node<ast::Program>,
4401 ) -> Result<ast::Expr, KclError> {
4402 let segment_object = self
4403 .scene_graph
4404 .objects
4405 .get(segment_id.0)
4406 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4407 let ObjectKind::Segment { segment } = &segment_object.kind else {
4408 return Err(KclError::refactor(format!(
4409 "Object is not a segment, it was {}",
4410 segment_object.kind.human_friendly_kind_with_article()
4411 )));
4412 };
4413
4414 match segment {
4415 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4416 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4417 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4418 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4419 Segment::ControlPointSpline(_) => Err(KclError::refactor(
4420 "Symmetric does not yet support control point splines".to_owned(),
4421 )),
4422 }
4423 }
4424
4425 fn symmetric_axis_id_to_ast_reference(
4426 &mut self,
4427 segment_id: ObjectId,
4428 new_ast: &mut ast::Node<ast::Program>,
4429 ) -> Result<ast::Expr, KclError> {
4430 let segment_object = self
4431 .scene_graph
4432 .objects
4433 .get(segment_id.0)
4434 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4435 let ObjectKind::Segment { segment } = &segment_object.kind else {
4436 return Err(KclError::refactor(format!(
4437 "Object is not a segment, it was {}",
4438 segment_object.kind.human_friendly_kind_with_article()
4439 )));
4440 };
4441 match segment {
4442 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4443 _ => Err(KclError::refactor(format!(
4444 "Symmetric axis must be a line, got {}",
4445 segment.human_friendly_kind_with_article()
4446 ))),
4447 }
4448 }
4449
4450 async fn add_parallel(
4451 &mut self,
4452 sketch: ObjectId,
4453 parallel: Parallel,
4454 new_ast: &mut ast::Node<ast::Program>,
4455 ) -> Result<AstNodeRef, KclError> {
4456 if parallel.lines.len() < 2 {
4457 return Err(KclError::refactor(format!(
4458 "Parallel constraint must have at least 2 lines, got {}",
4459 parallel.lines.len()
4460 )));
4461 };
4462
4463 let sketch_id = sketch;
4464
4465 let line_asts = parallel
4466 .lines
4467 .iter()
4468 .map(|line_id| {
4469 let line_object = self
4470 .scene_graph
4471 .objects
4472 .get(line_id.0)
4473 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4474 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4475 let kind = line_object.kind.human_friendly_kind_with_article();
4476 return Err(KclError::refactor(format!(
4477 "This constraint only works on Segments, but you selected {kind}"
4478 )));
4479 };
4480 let Segment::Line(_) = line_segment else {
4481 let kind = line_segment.human_friendly_kind_with_article();
4482 return Err(KclError::refactor(format!(
4483 "Only lines can be made parallel, but you selected {kind}"
4484 )));
4485 };
4486
4487 self.line_id_to_ast_reference(*line_id, new_ast)
4488 })
4489 .collect::<Result<Vec<_>, _>>()?;
4490
4491 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4492 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4493 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4494 ast::ArrayExpression {
4495 elements: line_asts,
4496 digest: None,
4497 non_code_meta: Default::default(),
4498 },
4499 )))),
4500 arguments: Default::default(),
4501 digest: None,
4502 non_code_meta: Default::default(),
4503 })));
4504
4505 let (sketch_block_ref, _) = self.mutate_ast(
4506 new_ast,
4507 sketch_id,
4508 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4509 )?;
4510 Ok(sketch_block_ref)
4511 }
4512
4513 async fn add_perpendicular(
4514 &mut self,
4515 sketch: ObjectId,
4516 perpendicular: Perpendicular,
4517 new_ast: &mut ast::Node<ast::Program>,
4518 ) -> Result<AstNodeRef, KclError> {
4519 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4520 .await
4521 }
4522
4523 async fn add_lines_at_angle_constraint(
4524 &mut self,
4525 sketch: ObjectId,
4526 angle_kind: LinesAtAngleKind,
4527 lines: Vec<ObjectId>,
4528 new_ast: &mut ast::Node<ast::Program>,
4529 ) -> Result<AstNodeRef, KclError> {
4530 let &[line0_id, line1_id] = lines.as_slice() else {
4531 return Err(KclError::refactor(format!(
4532 "{} constraint must have exactly 2 lines, got {}",
4533 angle_kind.to_function_name(),
4534 lines.len()
4535 )));
4536 };
4537
4538 let sketch_id = sketch;
4539
4540 let line0_object = self
4542 .scene_graph
4543 .objects
4544 .get(line0_id.0)
4545 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4546 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4547 let kind = line0_object.kind.human_friendly_kind_with_article();
4548 return Err(KclError::refactor(format!(
4549 "This constraint only works on Segments, but you selected {kind}"
4550 )));
4551 };
4552 let Segment::Line(_) = line0_segment else {
4553 return Err(KclError::refactor(format!(
4554 "Only lines can be made {}, but you selected {}",
4555 angle_kind.to_function_name(),
4556 line0_segment.human_friendly_kind_with_article(),
4557 )));
4558 };
4559 let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4560
4561 let line1_object = self
4562 .scene_graph
4563 .objects
4564 .get(line1_id.0)
4565 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4566 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4567 let kind = line1_object.kind.human_friendly_kind_with_article();
4568 return Err(KclError::refactor(format!(
4569 "This constraint only works on Segments, but you selected {kind}"
4570 )));
4571 };
4572 let Segment::Line(_) = line1_segment else {
4573 return Err(KclError::refactor(format!(
4574 "Only lines can be made {}, but you selected {}",
4575 angle_kind.to_function_name(),
4576 line1_segment.human_friendly_kind_with_article(),
4577 )));
4578 };
4579 let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4580
4581 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4583 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4584 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4585 ast::ArrayExpression {
4586 elements: vec![line0_ast, line1_ast],
4587 digest: None,
4588 non_code_meta: Default::default(),
4589 },
4590 )))),
4591 arguments: Default::default(),
4592 digest: None,
4593 non_code_meta: Default::default(),
4594 })));
4595
4596 let (sketch_block_ref, _) = self.mutate_ast(
4598 new_ast,
4599 sketch_id,
4600 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4601 )?;
4602 Ok(sketch_block_ref)
4603 }
4604
4605 async fn add_vertical(
4606 &mut self,
4607 sketch: ObjectId,
4608 vertical: Vertical,
4609 new_ast: &mut ast::Node<ast::Program>,
4610 ) -> Result<AstNodeRef, KclError> {
4611 let sketch_id = sketch;
4612
4613 let first_arg_ast = match vertical {
4614 Vertical::Line { line } => {
4615 let line_object = self
4617 .scene_graph
4618 .objects
4619 .get(line.0)
4620 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4621 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4622 let kind = line_object.kind.human_friendly_kind_with_article();
4623 return Err(KclError::refactor(format!(
4624 "This constraint only works on Segments, but you selected {kind}"
4625 )));
4626 };
4627 let Segment::Line(_) = line_segment else {
4628 return Err(KclError::refactor(format!(
4629 "Only lines can be made vertical, but you selected {}",
4630 line_segment.human_friendly_kind_with_article()
4631 )));
4632 };
4633 self.line_id_to_ast_reference(line, new_ast)?
4634 }
4635 Vertical::Points { points } => {
4636 let point_asts = points
4637 .iter()
4638 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4639 .collect::<Result<Vec<_>, _>>()?;
4640 ast::ArrayExpression::new(point_asts).into()
4641 }
4642 };
4643 let vertical_ast = create_vertical_ast(first_arg_ast);
4645
4646 let (sketch_block_ref, _) = self.mutate_ast(
4648 new_ast,
4649 sketch_id,
4650 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4651 )?;
4652 Ok(sketch_block_ref)
4653 }
4654
4655 async fn execute_after_add_constraint(
4656 &mut self,
4657 ctx: &ExecutorContext,
4658 sketch_id: ObjectId,
4659 sketch_block_ref: AstNodeRef,
4660 new_ast: &mut ast::Node<ast::Program>,
4661 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4662 let new_source = source_from_ast(new_ast);
4664 let new_program = parse_frontend_mutation_source(
4666 &new_source,
4667 "Error parsing KCL source after adding constraint",
4668 "No AST produced after adding constraint",
4669 )?;
4670 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4671 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4672 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4673 )))
4674 })?;
4675
4676 let mut truncated_program = new_program.clone();
4679 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4680 .map_err(KclErrorWithOutputs::no_outputs)?;
4681
4682 let outcome = ctx
4684 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4685 .await?;
4686
4687 let new_object_ids = {
4688 let constraint_id = outcome
4690 .source_range_to_object
4691 .get(&constraint_node_ref.range)
4692 .copied()
4693 .ok_or_else(|| {
4694 KclErrorWithOutputs::from_error_outcome(
4695 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4696 outcome.clone(),
4697 )
4698 })?;
4699 vec![constraint_id]
4700 };
4701
4702 self.program = new_program;
4705
4706 let outcome = self.update_state_after_exec(outcome, true);
4708
4709 let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4710 let scene_graph_delta = SceneGraphDelta {
4711 new_graph: self.scene_graph_for_ui(),
4712 invalidates_ids: false,
4713 new_objects: new_object_ids,
4714 exec_outcome: outcome,
4715 };
4716 Ok((src_delta, scene_graph_delta))
4717 }
4718
4719 fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4720 let commit_failure = || {
4721 KclErrorWithOutputs::from_error_outcome(
4722 KclError::refactor(format!("Could not update KCL after {operation}.")),
4723 outcome.clone(),
4724 )
4725 };
4726
4727 let default_length_unit = self.default_length_unit();
4728 let mut settled_ast = self.program.ast.clone();
4729 let mut committed_solver_value = false;
4730 for (var_range, node_path, value) in &outcome.var_solutions {
4731 let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4732 return Err(commit_failure());
4733 };
4734 let new_value = match &lookup {
4735 Some(current_literal) => {
4736 if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4737 continue;
4738 }
4739 preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4740 }
4741 None => {
4742 Number {
4746 value: number_value_in_default_length_units(*value, default_length_unit),
4747 units: default_length_unit.into(),
4748 }
4749 }
4750 };
4751 committed_solver_value = true;
4752 let source_ref = SourceRef::Simple {
4753 range: *var_range,
4754 node_path: node_path.clone(),
4755 };
4756 mutate_ast_node_by_source_ref(
4757 &mut settled_ast,
4758 &source_ref,
4759 AstMutateCommand::EditVarInitialValue { value: new_value },
4760 )
4761 .map_err(|_| commit_failure())?;
4762 }
4763
4764 if !committed_solver_value {
4765 return Ok(SourceDelta {
4766 text: self.program.original_file_contents.clone(),
4767 });
4768 }
4769
4770 let settled_source = source_from_ast(&settled_ast);
4771 let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4772 if !errors.is_empty() {
4773 return Err(commit_failure());
4774 }
4775 let Some(settled_program) = settled_program else {
4776 return Err(commit_failure());
4777 };
4778
4779 self.program = settled_program;
4780
4781 Ok(SourceDelta { text: settled_source })
4782 }
4783
4784 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4786 if segment_ids_set.contains(&segment_id) {
4787 return true;
4788 }
4789
4790 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4791 return false;
4792 };
4793 let ObjectKind::Segment { segment } = &segment_object.kind else {
4794 return false;
4795 };
4796 let Segment::Point(point) = segment else {
4797 return false;
4798 };
4799
4800 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4801 }
4802
4803 fn remaining_constraint_segments(
4804 &self,
4805 segments: &[ConstraintSegment],
4806 segment_ids_set: &AhashIndexSet<ObjectId>,
4807 ) -> Vec<ConstraintSegment> {
4808 segments
4809 .iter()
4810 .copied()
4811 .filter(|segment| match segment {
4812 ConstraintSegment::Origin(_) => true,
4813 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4814 })
4815 .collect()
4816 }
4817
4818 fn find_referenced_constraints(
4819 &self,
4820 sketch_id: ObjectId,
4821 segment_ids_set: &AhashIndexSet<ObjectId>,
4822 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4823 let sketch_object = self
4825 .scene_graph
4826 .objects
4827 .get(sketch_id.0)
4828 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4829 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4830 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4831 };
4832 let segment_or_owner_matches = |segment_id: ObjectId| {
4833 if segment_ids_set.contains(&segment_id) {
4834 return true;
4835 }
4836 let segment_object = self.scene_graph.objects.get(segment_id.0);
4837 if let Some(obj) = segment_object
4838 && let ObjectKind::Segment { segment } = &obj.kind
4839 {
4840 match segment {
4841 Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4842 Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4843 _ => false,
4844 }
4845 } else {
4846 false
4847 }
4848 };
4849 let mut constraint_ids_set = AhashIndexSet::default();
4850 for constraint_id in &sketch.constraints {
4851 let constraint_object = self
4852 .scene_graph
4853 .objects
4854 .get(constraint_id.0)
4855 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4856 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4857 return Err(KclError::refactor(format!(
4858 "Object is not a constraint, it is {}",
4859 constraint_object.kind.human_friendly_kind_with_article()
4860 )));
4861 };
4862 let depends_on_segment = match constraint {
4863 Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4864 Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4865 Constraint::Fixed(fixed) => fixed
4866 .points
4867 .iter()
4868 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4869 Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4870 Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4871 Constraint::EqualRadius(equal_radius) => {
4872 equal_radius.input.iter().copied().any(segment_or_owner_matches)
4873 }
4874 Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4875 Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4876 Constraint::Horizontal(h) => match h {
4877 Horizontal::Line { line } => segment_or_owner_matches(*line),
4878 Horizontal::Points { points } => points.iter().any(|point| match point {
4879 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4880 ConstraintSegment::Origin(_) => false,
4881 }),
4882 },
4883 Constraint::Vertical(v) => match v {
4884 Vertical::Line { line } => segment_or_owner_matches(*line),
4885 Vertical::Points { points } => points.iter().any(|point| match point {
4886 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4887 ConstraintSegment::Origin(_) => false,
4888 }),
4889 },
4890 Constraint::LinesEqualLength(lines_equal_length) => {
4891 lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4892 }
4893 Constraint::Midpoint(midpoint) => {
4894 segment_or_owner_matches(midpoint.segment)
4895 || matches!(
4896 midpoint.point,
4897 ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4898 )
4899 }
4900 Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4901 Constraint::Perpendicular(perpendicular) => {
4902 perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4903 }
4904 Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4905 Constraint::Symmetric(symmetric) => {
4906 segment_or_owner_matches(symmetric.axis)
4907 || symmetric.input.iter().copied().any(segment_or_owner_matches)
4908 }
4909 Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4910 };
4911 if depends_on_segment {
4912 constraint_ids_set.insert(*constraint_id);
4913 }
4914 }
4915 Ok(constraint_ids_set)
4916 }
4917
4918 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4919 let mut outcome = outcome;
4920 self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4921 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4922
4923 if freedom_analysis_ran {
4924 self.point_freedom_cache.clear();
4927 for new_obj in &new_objects {
4928 if let ObjectKind::Segment {
4929 segment: crate::front::Segment::Point(point),
4930 } = &new_obj.kind
4931 {
4932 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4933 }
4934 }
4935 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4936 self.scene_graph.objects = new_objects;
4938 } else {
4939 for old_obj in &self.scene_graph.objects {
4942 if let ObjectKind::Segment {
4943 segment: crate::front::Segment::Point(point),
4944 } = &old_obj.kind
4945 {
4946 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4947 }
4948 }
4949
4950 let mut updated_objects = Vec::with_capacity(new_objects.len());
4952 for new_obj in new_objects {
4953 let mut obj = new_obj;
4954 if let ObjectKind::Segment {
4955 segment: crate::front::Segment::Point(point),
4956 } = &mut obj.kind
4957 {
4958 let new_freedom = point.freedom;
4959 match new_freedom {
4965 Freedom::Free => {
4966 match self.point_freedom_cache.get(&obj.id).copied() {
4967 Some(Freedom::Conflict) => {
4968 }
4971 Some(Freedom::Fixed) => {
4972 point.freedom = Freedom::Fixed;
4974 }
4975 Some(Freedom::Free) => {
4976 }
4978 None => {
4979 }
4981 }
4982 }
4983 Freedom::Fixed => {
4984 }
4986 Freedom::Conflict => {
4987 }
4989 }
4990 self.point_freedom_cache.insert(obj.id, point.freedom);
4992 }
4993 updated_objects.push(obj);
4994 }
4995
4996 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4997 self.scene_graph.objects = updated_objects;
4998 }
4999 outcome
5000 }
5001
5002 fn mutate_ast(
5003 &mut self,
5004 ast: &mut ast::Node<ast::Program>,
5005 object_id: ObjectId,
5006 command: AstMutateCommand,
5007 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5008 let sketch_object = self
5009 .scene_graph
5010 .objects
5011 .get(object_id.0)
5012 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5013 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5014 }
5015
5016 fn mutate_constraint_label_position(
5017 &mut self,
5018 ast: &mut ast::Node<ast::Program>,
5019 constraint_id: ObjectId,
5020 label_position: Point2d<Number>,
5021 ) -> Result<(), KclError> {
5022 let object = self
5023 .scene_graph
5024 .objects
5025 .get(constraint_id.0)
5026 .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5027 if !matches!(
5028 &object.kind,
5029 ObjectKind::Constraint {
5030 constraint: Constraint::Distance(_)
5031 | Constraint::HorizontalDistance(_)
5032 | Constraint::VerticalDistance(_)
5033 | Constraint::Radius(_)
5034 | Constraint::Diameter(_)
5035 | Constraint::Angle(_),
5036 }
5037 ) {
5038 return Err(KclError::refactor(format!(
5039 "Object does not support labelPosition: {constraint_id:?}"
5040 )));
5041 }
5042
5043 let label_position = to_ast_point2d_number(&label_position)
5044 .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5045 self.mutate_ast(
5046 ast,
5047 constraint_id,
5048 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5049 )?;
5050 Ok(())
5051 }
5052}
5053
5054fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5055 let sketch_object = scene_graph
5057 .objects
5058 .get(sketch_id.0)
5059 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5060 let ObjectKind::Sketch(_) = &sketch_object.kind else {
5061 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5062 };
5063 expect_single_node_ref(sketch_object)
5064}
5065
5066fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5067 match &object.source {
5068 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5069 range: *range,
5070 node_path: node_path.clone(),
5071 }),
5072 SourceRef::BackTrace { ranges } => {
5073 let [range] = ranges.as_slice() else {
5074 return Err(KclError::refactor(format!(
5075 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5076 ranges.len()
5077 )));
5078 };
5079 Ok(AstNodeRef {
5080 range: range.0,
5081 node_path: range.1.clone(),
5082 })
5083 }
5084 }
5085}
5086
5087fn only_sketch_block_from_range(
5090 ast: &mut ast::Node<ast::Program>,
5091 sketch_block_range: SourceRange,
5092 edit_kind: ChangeKind,
5093) -> Result<(), KclError> {
5094 let r1 = sketch_block_range;
5095 let matches_range = |r2: SourceRange| -> bool {
5096 match edit_kind {
5099 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5100 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5102 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5103 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5105 }
5106 };
5107 let mut found = false;
5108 for item in ast.body.iter_mut() {
5109 match item {
5110 ast::BodyItem::ImportStatement(_) => {}
5111 ast::BodyItem::ExpressionStatement(node) => {
5112 if matches_range(SourceRange::from(&*node))
5113 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5114 {
5115 sketch_block.is_being_edited = true;
5116 found = true;
5117 break;
5118 }
5119 }
5120 ast::BodyItem::VariableDeclaration(node) => {
5121 if matches_range(SourceRange::from(&node.declaration.init))
5122 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5123 {
5124 sketch_block.is_being_edited = true;
5125 found = true;
5126 break;
5127 }
5128 }
5129 ast::BodyItem::TypeDeclaration(_) => {}
5130 ast::BodyItem::ReturnStatement(node) => {
5131 if matches_range(SourceRange::from(&node.argument))
5132 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5133 {
5134 sketch_block.is_being_edited = true;
5135 found = true;
5136 break;
5137 }
5138 }
5139 }
5140 }
5141 if !found {
5142 return Err(KclError::refactor(format!(
5143 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5144 )));
5145 }
5146
5147 Ok(())
5148}
5149
5150fn only_sketch_block(
5151 ast: &mut ast::Node<ast::Program>,
5152 sketch_block_ref: &AstNodeRef,
5153 edit_kind: ChangeKind,
5154) -> Result<(), KclError> {
5155 let Some(target_node_path) = &sketch_block_ref.node_path else {
5156 #[cfg(target_arch = "wasm32")]
5157 web_sys::console::warn_1(
5158 &format!(
5159 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5160 sketch_block_ref
5161 )
5162 .into(),
5163 );
5164 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5165 };
5166 struct MarkSketchBlockBeingEdited<'a> {
5167 target_node_path: &'a ast::NodePath,
5168 }
5169
5170 impl Visitor for MarkSketchBlockBeingEdited<'_> {
5171 type Break = ();
5172 type Continue = ();
5173
5174 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5175 if let NodeMut::SketchBlock(sketch_block) = node
5176 && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5177 {
5178 sketch_block.is_being_edited = true;
5179 return TraversalReturn::new_break(());
5180 }
5181 TraversalReturn::new_continue(())
5182 }
5183
5184 fn finish(&mut self, _node: NodeMut<'_>) {}
5185 }
5186
5187 let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5188 let found = dfs_mut(ast, &mut marker).is_break();
5189 if !found {
5190 return Err(KclError::refactor(format!(
5191 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5192 )));
5193 }
5194
5195 Ok(())
5196}
5197
5198fn sketch_on_ast_expr(
5199 ast: &mut ast::Node<ast::Program>,
5200 scene_graph: &SceneGraph,
5201 solid_references: &HashMap<Uuid, SolidAstReference>,
5202 on: &Plane,
5203) -> Result<ast::Expr, KclError> {
5204 match on {
5205 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5206 Plane::Object(object_id) => {
5207 let on_object = scene_graph
5208 .objects
5209 .get(object_id.0)
5210 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5211 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5212 return Ok(face_expr);
5213 }
5214 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5215 }
5216 Plane::PrimitiveFace(face) => {
5217 let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5218 KclError::refactor(format!(
5219 "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5220 face.solid_id
5221 ))
5222 })?;
5223 let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5224 Ok(create_face_of_ast(solid_expr, face_id_expr))
5225 }
5226 }
5227}
5228
5229fn solid_references_from_variables(
5230 ast: &ast::Node<ast::Program>,
5231 variables: &IndexMap<String, KclValueView>,
5232) -> HashMap<Uuid, SolidAstReference> {
5233 let mut references = HashMap::new();
5234
5235 for item in &ast.body {
5238 let ast::BodyItem::VariableDeclaration(declaration) = item else {
5239 continue;
5240 };
5241 let name = &declaration.declaration.id.name;
5242 let Some(value) = variables.get(name) else {
5243 continue;
5244 };
5245
5246 match value {
5247 KclValueView::Solid { value } => {
5248 references.insert(
5249 value.id,
5250 SolidAstReference {
5251 variable_name: name.clone(),
5252 output_index: None,
5253 },
5254 );
5255 }
5256 KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5257 for (output_index, entry) in value.iter().enumerate() {
5258 if let KclValueView::Solid { value } = entry {
5259 references.insert(
5260 value.id,
5261 SolidAstReference {
5262 variable_name: name.clone(),
5263 output_index: Some(output_index),
5264 },
5265 );
5266 }
5267 }
5268 }
5269 _ => {}
5270 }
5271 }
5272
5273 references
5274}
5275
5276fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5277 let reference = solid_references.get(&solid_id)?;
5278 let solid_expr = ast_name_expr(reference.variable_name.clone());
5279 Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5280}
5281
5282fn sketch_face_of_scene_object_ast_expr(
5283 ast: &mut ast::Node<ast::Program>,
5284 on_object: &crate::front::Object,
5285) -> Result<Option<ast::Expr>, KclError> {
5286 match &on_object.kind {
5287 ObjectKind::Wall(wall) => {
5288 let solid_ref = get_or_insert_ast_reference(
5289 ast,
5290 &source_ref_from_source_ref_range(&wall.source.solid),
5291 "solid",
5292 None,
5293 )?;
5294 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5295 return Err(KclError::refactor(format!(
5296 "Could not resolve solid reference for selected wall: artifact_id={:?}",
5297 on_object.artifact_id
5298 )));
5299 };
5300 let solid_expr = indexed_solid_expr_for_sweep_output(
5301 ast_name_expr(solid_name_expr.name.name.clone()),
5302 wall.solid_output_index,
5303 );
5304 let sweep_ref = get_or_insert_ast_reference(
5305 ast,
5306 &source_ref_from_source_ref_range(&wall.source.sweep),
5307 "solid",
5308 None,
5309 )?;
5310 let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5311 return Err(KclError::refactor(format!(
5312 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5313 on_object.artifact_id
5314 )));
5315 };
5316 let sweep_name = sweep_name_expr.name.name.clone();
5317 let segment_ref = get_or_insert_ast_reference(
5318 ast,
5319 &source_ref_from_source_ref_range(&wall.source.segment),
5320 LINE_VARIABLE,
5321 None,
5322 )?;
5323
5324 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5325 wall.source
5326 .path
5327 .as_ref()
5328 .and_then(|path_source| region_name_from_path_source(ast, path_source))
5329 }) {
5330 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5331 return Err(KclError::refactor(format!(
5332 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5333 on_object.artifact_id
5334 )));
5335 };
5336 create_member_expression(
5337 create_member_expression(ast_name_expr(region_name), "tags"),
5338 &segment_name_expr.name.name,
5339 )
5340 } else {
5341 segment_ref
5342 };
5343
5344 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5345 }
5346 ObjectKind::Cap(cap) => {
5347 let solid_ref =
5348 get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5349 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5350 return Err(KclError::refactor(format!(
5351 "Could not resolve solid reference for selected cap: artifact_id={:?}",
5352 on_object.artifact_id
5353 )));
5354 };
5355 let solid_expr = indexed_solid_expr_for_sweep_output(
5356 ast_name_expr(solid_name_expr.name.name.clone()),
5357 cap.solid_output_index,
5358 );
5359 let face_expr = match cap.kind {
5361 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5362 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5363 };
5364
5365 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5366 }
5367 _ => Ok(None),
5368 }
5369}
5370
5371fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5372 match solid_output_index {
5373 Some(output_index) => create_index_expression(solid_expr, output_index),
5374 None => solid_expr,
5375 }
5376}
5377
5378fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5379 SourceRef::Simple {
5380 range: source.range,
5381 node_path: source.node_path.clone(),
5382 }
5383}
5384
5385fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5386 let source_ref = source_ref_from_source_ref_range(path_source);
5387 let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5388 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5389 return None;
5390 };
5391 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5392 return None;
5393 };
5394 if region_call.callee.name.name != "region" {
5395 return None;
5396 }
5397 Some(candidate)
5398}
5399
5400fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5401 let mut current_id = source_id;
5402 let mut current_composite = None;
5403 let mut visited = HashSet::new();
5404
5405 while visited.insert(current_id) {
5406 let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5407
5408 let Some(composite_id) = next_composite_id else {
5409 break;
5410 };
5411 let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5412 break;
5413 };
5414
5415 current_id = composite.id;
5416 current_composite = Some(composite);
5417
5418 if !composite.consumed {
5419 break;
5420 }
5421 }
5422
5423 current_composite.map(|composite| &composite.code_ref)
5424}
5425
5426fn downstream_composite_id_for_solid_source(
5427 artifact_graph: &ArtifactGraph,
5428 source_id: ArtifactId,
5429) -> Option<ArtifactId> {
5430 if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5432 && let Some(composite_id) = path.composite_solid_id
5433 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5434 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5435 {
5436 return Some(composite_id);
5437 }
5438
5439 for artifact in artifact_graph.values() {
5441 if let Artifact::Path(path) = artifact
5442 && path.sweep_id == Some(source_id)
5443 && let Some(composite_id) = path.composite_solid_id
5444 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5445 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5446 {
5447 return Some(composite_id);
5448 }
5449 }
5450
5451 artifact_graph.values().find_map(|artifact| {
5453 let Artifact::CompositeSolid(composite) = artifact else {
5454 return None;
5455 };
5456 composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5457 })
5458}
5459
5460fn composite_contains_path_input(
5461 solid_ids: &[ArtifactId],
5462 tool_ids: &[ArtifactId],
5463 path_id: ArtifactId,
5464 solid2d_id: Option<ArtifactId>,
5465) -> bool {
5466 composite_contains_input(solid_ids, tool_ids, path_id)
5467 || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5468}
5469
5470fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5471 solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5472}
5473
5474fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5475 let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5476 SourceRefRange {
5477 range: code_ref.range,
5478 node_path,
5479 }
5480}
5481
5482fn solid_output_index_for_sweep(
5483 artifact_graph: &ArtifactGraph,
5484 sweep_id: ArtifactId,
5485 sweep_code_ref: &CodeRef,
5486) -> Option<usize> {
5487 if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5490 return None;
5491 }
5492
5493 let sibling_sweeps = artifact_graph
5494 .values()
5495 .filter_map(|artifact| match artifact {
5496 Artifact::Sweep(sweep)
5497 if sweep.code_ref.range == sweep_code_ref.range
5498 && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5499 {
5500 Some(sweep)
5501 }
5502 _ => None,
5503 })
5504 .collect::<Vec<_>>();
5505
5506 if sibling_sweeps.len() <= 1 {
5507 return None;
5508 }
5509
5510 sibling_sweeps
5511 .iter()
5512 .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5513}
5514
5515fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5516 let mut existing_artifact_ids = scene_objects
5517 .iter()
5518 .map(|object| object.artifact_id)
5519 .collect::<HashSet<_>>();
5520
5521 for artifact in artifact_graph.values() {
5522 match artifact {
5523 Artifact::Wall(wall) => {
5524 if existing_artifact_ids.contains(&wall.id) {
5525 continue;
5526 }
5527
5528 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5529 Artifact::Segment(segment) => Some(segment),
5530 _ => None,
5531 }) else {
5532 continue;
5533 };
5534 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5535 Artifact::Sweep(sweep) => Some(sweep),
5536 _ => None,
5537 }) else {
5538 continue;
5539 };
5540 let source_segment = segment
5541 .original_seg_id
5542 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5543 .and_then(|artifact| match artifact {
5544 Artifact::Segment(segment) => Some(segment),
5545 _ => None,
5546 })
5547 .unwrap_or(segment);
5548 let solid_code_ref =
5549 downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5550 let path_code_ref = artifact_graph
5551 .get(&segment.path_id)
5552 .or_else(|| artifact_graph.get(&sweep.path_id))
5553 .and_then(|artifact| match artifact {
5554 Artifact::Path(path) => Some(&path.code_ref),
5555 _ => None,
5556 });
5557 let source = WallSource {
5558 solid: code_ref_source_ref_range(solid_code_ref),
5559 sweep: code_ref_source_ref_range(&sweep.code_ref),
5560 path: path_code_ref.map(code_ref_source_ref_range),
5561 segment: code_ref_source_ref_range(&source_segment.code_ref),
5562 };
5563 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5564 && solid_code_ref.node_path == sweep.code_ref.node_path)
5565 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5566 .flatten();
5567 let object_source = source_ref_from_source_ref_range(&source.solid);
5568 let id = ObjectId(scene_objects.len());
5569 scene_objects.push(crate::front::Object {
5570 id,
5571 kind: ObjectKind::Wall(crate::frontend::api::Wall {
5572 id,
5573 source,
5574 solid_output_index,
5575 }),
5576 label: Default::default(),
5577 comments: Default::default(),
5578 artifact_id: wall.id,
5579 source: object_source,
5580 });
5581 existing_artifact_ids.insert(wall.id);
5582 }
5583 Artifact::Cap(cap) => {
5584 if existing_artifact_ids.contains(&cap.id) {
5585 continue;
5586 }
5587
5588 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5589 Artifact::Sweep(sweep) => Some(sweep),
5590 _ => None,
5591 }) else {
5592 continue;
5593 };
5594 let id = ObjectId(scene_objects.len());
5595 let kind = match cap.sub_type {
5596 CapSubType::Start => crate::frontend::api::CapKind::Start,
5597 CapSubType::End => crate::frontend::api::CapKind::End,
5598 };
5599 let solid_code_ref =
5600 downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5601 let source = CapSource {
5602 solid: code_ref_source_ref_range(solid_code_ref),
5603 sweep: code_ref_source_ref_range(&sweep.code_ref),
5604 };
5605 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5606 && solid_code_ref.node_path == sweep.code_ref.node_path)
5607 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5608 .flatten();
5609 let object_source = source_ref_from_source_ref_range(&source.solid);
5610 scene_objects.push(crate::front::Object {
5611 id,
5612 kind: ObjectKind::Cap(crate::frontend::api::Cap {
5613 id,
5614 kind,
5615 source,
5616 solid_output_index,
5617 }),
5618 label: Default::default(),
5619 comments: Default::default(),
5620 artifact_id: cap.id,
5621 source: object_source,
5622 });
5623 existing_artifact_ids.insert(cap.id);
5624 }
5625 _ => {}
5626 }
5627 }
5628}
5629
5630fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5631 use crate::engine::PlaneName;
5632
5633 match name {
5634 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5635 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5636 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5637 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5638 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5639 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5640 }
5641}
5642
5643fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5644 ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5645 ast::UnaryOperator::Neg,
5646 ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5647 )))
5648}
5649
5650fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5651 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5652 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5653 unlabeled: Some(solid_expr),
5654 arguments: vec![ast::LabeledArg {
5655 label: Some(ast::Identifier::new("face")),
5656 arg: face_expr,
5657 }],
5658 digest: None,
5659 non_code_meta: Default::default(),
5660 })))
5661}
5662
5663fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5664 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5665 callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5666 unlabeled: Some(solid_expr),
5667 arguments: vec![ast::LabeledArg {
5668 label: Some(ast::Identifier::new("index")),
5669 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5670 ast::NumericLiteral {
5671 value: index as f64,
5672 suffix: NumericSuffix::None,
5673 raw: index.to_string(),
5674 digest: None,
5675 },
5676 )))),
5677 }],
5678 digest: None,
5679 non_code_meta: Default::default(),
5680 })))
5681}
5682
5683fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5684 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5685 return None;
5686 };
5687 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5688 return None;
5689 };
5690 if !matches!(
5691 sweep_call.callee.name.name.as_str(),
5692 "extrude" | "revolve" | "sweep" | "loft"
5693 ) {
5694 return None;
5695 }
5696 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5697 return None;
5698 };
5699 let candidate = region_name_expr.name.name.clone();
5700 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5701 return None;
5702 };
5703 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5704 return None;
5705 };
5706 if region_call.callee.name.name != "region" {
5707 return None;
5708 }
5709 Some(candidate)
5710}
5711
5712fn get_or_insert_ast_reference(
5719 ast: &mut ast::Node<ast::Program>,
5720 source_ref: &SourceRef,
5721 prefix: &str,
5722 property: Option<&str>,
5723) -> Result<ast::Expr, KclError> {
5724 let command = AstMutateCommand::AddVariableDeclaration {
5725 prefix: prefix.to_owned(),
5726 };
5727 let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5728 Ok((_, ret)) => ret,
5729 Err(err) => {
5730 if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5731 AstMutateCommandReturn::Name(var_name)
5732 } else {
5733 return Err(err);
5734 }
5735 }
5736 };
5737 let AstMutateCommandReturn::Name(var_name) = ret else {
5738 return Err(KclError::refactor(
5739 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5740 ));
5741 };
5742 let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5743 let Some(property) = property else {
5744 return Ok(var_expr);
5746 };
5747
5748 Ok(create_member_expression(var_expr, property))
5749}
5750
5751fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5752 let source_range = match source_ref {
5753 SourceRef::Simple { range, .. } => *range,
5754 SourceRef::BackTrace { ranges } => {
5755 let [range] = ranges.as_slice() else {
5756 return None;
5757 };
5758 range.0
5759 }
5760 };
5761 ast.body.iter().find_map(|item| {
5762 let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5763 return None;
5764 };
5765 let init_range = SourceRange::from(&var_decl.declaration.init);
5766 let source_is_inside_init = init_range.module_id() == source_range.module_id()
5767 && init_range.start() <= source_range.start()
5768 && source_range.end() <= init_range.end();
5769 if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5770 && init_range != source_range
5771 && source_is_inside_init
5772 {
5773 return None;
5774 }
5775 source_is_inside_init.then(|| var_decl.name().to_owned())
5776 })
5777}
5778
5779fn mutate_ast_node_by_source_ref(
5780 ast: &mut ast::Node<ast::Program>,
5781 source_ref: &SourceRef,
5782 command: AstMutateCommand,
5783) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5784 let (source_range, node_path) = match source_ref {
5785 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5786 SourceRef::BackTrace { ranges } => {
5787 let [range] = ranges.as_slice() else {
5788 return Err(KclError::refactor(format!(
5789 "Expected single source ref, got {}; ranges={ranges:#?}",
5790 ranges.len(),
5791 )));
5792 };
5793 (range.0, range.1.clone())
5794 }
5795 };
5796 let mut context = AstMutateContext {
5797 source_range,
5798 node_path,
5799 command,
5800 defined_names_stack: Default::default(),
5801 };
5802 let control = dfs_mut(ast, &mut context);
5803 match control {
5804 ControlFlow::Continue(_) => Err(KclError::refactor(
5805 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5806 )),
5807 ControlFlow::Break(break_value) => break_value,
5808 }
5809}
5810
5811#[derive(Debug)]
5812struct AstMutateContext {
5813 source_range: SourceRange,
5814 node_path: Option<ast::NodePath>,
5815 command: AstMutateCommand,
5816 defined_names_stack: Vec<HashSet<String>>,
5817}
5818
5819#[derive(Debug)]
5820#[allow(clippy::large_enum_variant)]
5821enum AstMutateCommand {
5822 AddSketchBlockExprStmt {
5824 expr: ast::Expr,
5825 },
5826 AddSketchBlockVarDecl {
5828 prefix: String,
5829 expr: ast::Expr,
5830 },
5831 AddVariableDeclaration {
5832 prefix: String,
5833 },
5834 EditPoint {
5835 at: ast::Expr,
5836 },
5837 EditLine {
5838 start: ast::Expr,
5839 end: ast::Expr,
5840 construction: Option<bool>,
5841 },
5842 EditArc {
5843 start: ast::Expr,
5844 end: ast::Expr,
5845 center: ast::Expr,
5846 direction: Option<ArcDirection>,
5847 construction: Option<bool>,
5848 },
5849 EditCircle {
5850 start: ast::Expr,
5851 center: ast::Expr,
5852 construction: Option<bool>,
5853 },
5854 EditControlPointSpline {
5855 points: ast::Expr,
5856 construction: Option<bool>,
5857 },
5858 EditConstraintValue {
5859 value: ast::BinaryPart,
5860 },
5861 EditAngleConstraint {
5862 call: ast::BinaryPart,
5863 value: ast::BinaryPart,
5864 },
5865 EditDistanceConstraint {
5866 call: ast::BinaryPart,
5867 value: ast::BinaryPart,
5868 },
5869 EditDistanceConstraintLabelPosition {
5870 label_position: ast::Expr,
5871 },
5872 EditCallUnlabeled {
5873 arg: ast::Expr,
5874 },
5875 EditVarInitialValue {
5876 value: Number,
5877 },
5878 DeleteNode,
5879}
5880
5881impl AstMutateCommand {
5882 fn needs_defined_names_stack(&self) -> bool {
5883 matches!(
5884 self,
5885 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5886 )
5887 }
5888}
5889
5890#[derive(Debug)]
5891enum AstMutateCommandReturn {
5892 None,
5893 Name(String),
5894}
5895
5896#[derive(Debug, Clone)]
5897struct AstNodeRef {
5898 range: SourceRange,
5899 node_path: Option<ast::NodePath>,
5900}
5901
5902impl<T> From<&ast::Node<T>> for AstNodeRef {
5903 fn from(value: &ast::Node<T>) -> Self {
5904 AstNodeRef {
5905 range: value.into(),
5906 node_path: value.node_path.clone(),
5907 }
5908 }
5909}
5910
5911impl From<&ast::BodyItem> for AstNodeRef {
5912 fn from(value: &ast::BodyItem) -> Self {
5913 match value {
5914 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5915 range: node.into(),
5916 node_path: node.node_path.clone(),
5917 },
5918 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5919 range: node.into(),
5920 node_path: node.node_path.clone(),
5921 },
5922 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5923 range: node.into(),
5924 node_path: node.node_path.clone(),
5925 },
5926 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5927 range: node.into(),
5928 node_path: node.node_path.clone(),
5929 },
5930 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5931 range: node.into(),
5932 node_path: node.node_path.clone(),
5933 },
5934 }
5935 }
5936}
5937
5938impl From<&ast::Expr> for AstNodeRef {
5939 fn from(value: &ast::Expr) -> Self {
5940 AstNodeRef {
5941 range: SourceRange::from(value),
5942 node_path: value.node_path().cloned(),
5943 }
5944 }
5945}
5946
5947impl From<&AstMutateContext> for AstNodeRef {
5948 fn from(value: &AstMutateContext) -> Self {
5949 AstNodeRef {
5950 range: value.source_range,
5951 node_path: value.node_path.clone(),
5952 }
5953 }
5954}
5955
5956impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5957 type Error = crate::walk::AstNodeError;
5958
5959 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5960 Ok(AstNodeRef {
5961 range: SourceRange::try_from(value)?,
5962 node_path: value.try_into()?,
5963 })
5964 }
5965}
5966
5967impl From<AstNodeRef> for SourceRange {
5968 fn from(value: AstNodeRef) -> Self {
5969 value.range
5970 }
5971}
5972
5973impl Visitor for AstMutateContext {
5974 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5975 type Continue = ();
5976
5977 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5978 filter_and_process(self, node)
5979 }
5980
5981 fn finish(&mut self, node: NodeMut<'_>) {
5982 match &node {
5983 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5984 self.defined_names_stack.pop();
5985 }
5986 _ => {}
5987 }
5988 }
5989}
5990
5991fn filter_and_process(
5992 ctx: &mut AstMutateContext,
5993 node: NodeMut,
5994) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5995 let Ok(node_range) = SourceRange::try_from(&node) else {
5996 return TraversalReturn::new_continue(());
5998 };
5999 if let NodeMut::VariableDeclaration(var_decl) = &node {
6004 let expr_range = SourceRange::from(&var_decl.declaration.init);
6005 let expr_node_path = var_decl.declaration.init.node_path();
6006 if source_ref_matches(ctx, expr_range, expr_node_path) {
6007 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6008 return TraversalReturn::new_break(Ok((
6011 AstNodeRef::from(&**var_decl),
6012 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6013 )));
6014 }
6015 if let AstMutateCommand::DeleteNode = &ctx.command {
6016 return TraversalReturn {
6019 mutate_body_item: MutateBodyItem::Delete,
6020 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6021 };
6022 }
6023 }
6024 }
6025 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6028 let expr_range = SourceRange::from(&expr_stmt.expression);
6029 let expr_node_path = expr_stmt.expression.node_path();
6030 if source_ref_matches(ctx, expr_range, expr_node_path) {
6031 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6032 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6035 return TraversalReturn::new_continue(());
6036 };
6037 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6038 }
6039 if let AstMutateCommand::DeleteNode = &ctx.command {
6040 return TraversalReturn {
6043 mutate_body_item: MutateBodyItem::Delete,
6044 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6045 };
6046 }
6047 }
6048 }
6049
6050 if ctx.command.needs_defined_names_stack() {
6051 if let NodeMut::Program(program) = &node {
6052 ctx.defined_names_stack.push(find_defined_names(*program));
6053 } else if let NodeMut::SketchBlock(block) = &node {
6054 ctx.defined_names_stack.push(find_defined_names(&block.body));
6055 }
6056 }
6057
6058 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6060 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6061 return TraversalReturn::new_continue(());
6062 }
6063 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6064 return TraversalReturn::new_continue(());
6065 };
6066 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6067}
6068
6069fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6070 match &ctx.node_path {
6071 Some(target) => Some(target) == node_path,
6072 None => node_range == ctx.source_range,
6073 }
6074}
6075
6076fn is_angle_constraint_call_name(name: &str) -> bool {
6077 matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6078}
6079
6080fn is_distance_constraint_call_name(name: &str) -> bool {
6081 matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6082}
6083
6084fn is_constraint_call_name(name: &str) -> bool {
6085 matches!(
6086 name,
6087 DISTANCE_FN
6088 | HORIZONTAL_DISTANCE_FN
6089 | VERTICAL_DISTANCE_FN
6090 | RADIUS_FN
6091 | DIAMETER_FN
6092 | ANGLE_FN
6093 | ANGLE_DIMENSION_FN
6094 )
6095}
6096
6097fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6098 if let ast::BinaryPart::CallExpressionKw(call) = part
6099 && is_constraint_call_name(call.callee.name.name.as_str())
6100 {
6101 Some(call)
6102 } else {
6103 None
6104 }
6105}
6106
6107fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6108 match &ctx.command {
6109 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6110 if let NodeMut::SketchBlock(sketch_block) = node {
6111 sketch_block
6112 .body
6113 .items
6114 .push(ast::BodyItem::ExpressionStatement(ast::Node {
6115 inner: ast::ExpressionStatement {
6116 expression: expr.clone(),
6117 digest: None,
6118 },
6119 start: Default::default(),
6120 end: Default::default(),
6121 module_id: Default::default(),
6122 node_path: None,
6123 outer_attrs: Default::default(),
6124 pre_comments: Default::default(),
6125 comment_start: Default::default(),
6126 }));
6127 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6128 }
6129 }
6130 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6131 if let NodeMut::SketchBlock(sketch_block) = node {
6132 let empty_defined_names = HashSet::new();
6133 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6134 let Ok(name) = next_free_name(prefix, defined_names) else {
6135 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6136 };
6137 sketch_block
6138 .body
6139 .items
6140 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6141 ast::VariableDeclaration::new(
6142 ast::VariableDeclarator::new(&name, expr.clone()),
6143 ast::ItemVisibility::Default,
6144 ast::VariableKind::Const,
6145 ),
6146 ))));
6147 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6148 }
6149 }
6150 AstMutateCommand::AddVariableDeclaration { prefix } => {
6151 if let NodeMut::VariableDeclaration(inner) = node {
6152 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6153 }
6154 if let NodeMut::ExpressionStatement(expr_stmt) = node {
6155 let empty_defined_names = HashSet::new();
6156 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6157 let Ok(name) = next_free_name(prefix, defined_names) else {
6158 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6160 };
6161 let mutate_node =
6162 ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6163 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6164 ast::ItemVisibility::Default,
6165 ast::VariableKind::Const,
6166 ))));
6167 return TraversalReturn {
6168 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6169 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6170 };
6171 }
6172 }
6173 AstMutateCommand::EditPoint { at } => {
6174 if let NodeMut::CallExpressionKw(call) = node {
6175 if call.callee.name.name != POINT_FN {
6176 return TraversalReturn::new_continue(());
6177 }
6178 for labeled_arg in &mut call.arguments {
6180 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6181 labeled_arg.arg = at.clone();
6182 }
6183 }
6184 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6185 }
6186 }
6187 AstMutateCommand::EditLine {
6188 start,
6189 end,
6190 construction,
6191 } => {
6192 if let NodeMut::CallExpressionKw(call) = node {
6193 if call.callee.name.name != LINE_FN {
6194 return TraversalReturn::new_continue(());
6195 }
6196 for labeled_arg in &mut call.arguments {
6198 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6199 labeled_arg.arg = start.clone();
6200 }
6201 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6202 labeled_arg.arg = end.clone();
6203 }
6204 }
6205 if let Some(construction_value) = construction {
6207 let construction_exists = call
6208 .arguments
6209 .iter()
6210 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6211 if *construction_value {
6212 if construction_exists {
6214 for labeled_arg in &mut call.arguments {
6216 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6217 labeled_arg.arg =
6218 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6219 value: ast::LiteralValue::Bool(true),
6220 raw: "true".to_string(),
6221 digest: None,
6222 })));
6223 }
6224 }
6225 } else {
6226 call.arguments.push(ast::LabeledArg {
6228 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6229 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6230 value: ast::LiteralValue::Bool(true),
6231 raw: "true".to_string(),
6232 digest: None,
6233 }))),
6234 });
6235 }
6236 } else {
6237 call.arguments
6239 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6240 }
6241 }
6242 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6243 }
6244 }
6245 AstMutateCommand::EditArc {
6246 start,
6247 end,
6248 center,
6249 direction,
6250 construction,
6251 } => {
6252 if let NodeMut::CallExpressionKw(call) = node {
6253 if call.callee.name.name != ARC_FN {
6254 return TraversalReturn::new_continue(());
6255 }
6256 for labeled_arg in &mut call.arguments {
6258 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6259 labeled_arg.arg = start.clone();
6260 }
6261 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6262 labeled_arg.arg = end.clone();
6263 }
6264 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6265 labeled_arg.arg = center.clone();
6266 }
6267 }
6268 if let Some(direction_value) = direction {
6270 let direction_exists = call
6271 .arguments
6272 .iter()
6273 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6274 if direction_value.is_clockwise() {
6275 let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6276 if direction_exists {
6277 for labeled_arg in &mut call.arguments {
6279 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6280 labeled_arg.arg = direction_ast.clone();
6281 }
6282 }
6283 } else {
6284 call.arguments.push(ast::LabeledArg {
6286 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6287 arg: direction_ast,
6288 });
6289 }
6290 } else {
6291 call.arguments
6294 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6295 }
6296 }
6297 if let Some(construction_value) = construction {
6299 let construction_exists = call
6300 .arguments
6301 .iter()
6302 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6303 if *construction_value {
6304 if construction_exists {
6306 for labeled_arg in &mut call.arguments {
6308 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6309 labeled_arg.arg =
6310 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6311 value: ast::LiteralValue::Bool(true),
6312 raw: "true".to_string(),
6313 digest: None,
6314 })));
6315 }
6316 }
6317 } else {
6318 call.arguments.push(ast::LabeledArg {
6320 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6321 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6322 value: ast::LiteralValue::Bool(true),
6323 raw: "true".to_string(),
6324 digest: None,
6325 }))),
6326 });
6327 }
6328 } else {
6329 call.arguments
6331 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6332 }
6333 }
6334 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6335 }
6336 }
6337 AstMutateCommand::EditCircle {
6338 start,
6339 center,
6340 construction,
6341 } => {
6342 if let NodeMut::CallExpressionKw(call) = node {
6343 if call.callee.name.name != CIRCLE_FN {
6344 return TraversalReturn::new_continue(());
6345 }
6346 for labeled_arg in &mut call.arguments {
6348 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6349 labeled_arg.arg = start.clone();
6350 }
6351 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6352 labeled_arg.arg = center.clone();
6353 }
6354 }
6355 if let Some(construction_value) = construction {
6357 let construction_exists = call
6358 .arguments
6359 .iter()
6360 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6361 if *construction_value {
6362 if construction_exists {
6363 for labeled_arg in &mut call.arguments {
6365 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6366 labeled_arg.arg =
6367 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6368 value: ast::LiteralValue::Bool(true),
6369 raw: "true".to_string(),
6370 digest: None,
6371 })));
6372 }
6373 }
6374 } else {
6375 call.arguments.push(ast::LabeledArg {
6377 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6378 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6379 value: ast::LiteralValue::Bool(true),
6380 raw: "true".to_string(),
6381 digest: None,
6382 }))),
6383 });
6384 }
6385 } else {
6386 call.arguments
6388 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6389 }
6390 }
6391 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6392 }
6393 }
6394 AstMutateCommand::EditControlPointSpline { points, construction } => {
6395 if let NodeMut::CallExpressionKw(call) = node {
6396 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6397 return TraversalReturn::new_continue(());
6398 }
6399 for labeled_arg in &mut call.arguments {
6400 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6401 {
6402 labeled_arg.arg = points.clone();
6403 }
6404 }
6405 if let Some(construction_value) = construction {
6407 let construction_exists = call
6408 .arguments
6409 .iter()
6410 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6411 if *construction_value {
6412 if construction_exists {
6413 for labeled_arg in &mut call.arguments {
6414 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6415 labeled_arg.arg =
6416 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6417 value: ast::LiteralValue::Bool(true),
6418 raw: "true".to_string(),
6419 digest: None,
6420 })));
6421 }
6422 }
6423 } else {
6424 call.arguments.push(ast::LabeledArg {
6425 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6426 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6427 value: ast::LiteralValue::Bool(true),
6428 raw: "true".to_string(),
6429 digest: None,
6430 }))),
6431 });
6432 }
6433 } else {
6434 call.arguments
6435 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6436 }
6437 }
6438 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6439 }
6440 }
6441 AstMutateCommand::EditConstraintValue { value } => {
6442 if let NodeMut::BinaryExpression(binary_expr) = node {
6443 let left_is_constraint = matches!(
6444 &binary_expr.left,
6445 ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6446 );
6447 if left_is_constraint {
6448 binary_expr.right = value.clone();
6449 } else {
6450 binary_expr.left = value.clone();
6451 }
6452
6453 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6454 }
6455 }
6456 AstMutateCommand::EditAngleConstraint { call, value } => {
6457 if let NodeMut::BinaryExpression(binary_expr) = node {
6458 let left_is_angle = matches!(
6459 &binary_expr.left,
6460 ast::BinaryPart::CallExpressionKw(existing_call)
6461 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6462 );
6463 let right_is_angle = matches!(
6464 &binary_expr.right,
6465 ast::BinaryPart::CallExpressionKw(existing_call)
6466 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6467 );
6468
6469 match (left_is_angle, right_is_angle) {
6470 (true, _) => {
6471 binary_expr.left = call.clone();
6472 binary_expr.right = value.clone();
6473 }
6474 (false, true) => {
6475 binary_expr.left = value.clone();
6476 binary_expr.right = call.clone();
6477 }
6478 (false, false) => return TraversalReturn::new_continue(()),
6479 }
6480
6481 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6482 }
6483 }
6484 AstMutateCommand::EditDistanceConstraint { call, value } => {
6485 if let NodeMut::BinaryExpression(binary_expr) = node {
6486 let left_is_distance = matches!(
6487 &binary_expr.left,
6488 ast::BinaryPart::CallExpressionKw(existing_call)
6489 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6490 );
6491 let right_is_distance = matches!(
6492 &binary_expr.right,
6493 ast::BinaryPart::CallExpressionKw(existing_call)
6494 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6495 );
6496
6497 match (left_is_distance, right_is_distance) {
6498 (true, _) => {
6499 binary_expr.left = call.clone();
6500 binary_expr.right = value.clone();
6501 }
6502 (false, true) => {
6503 binary_expr.left = value.clone();
6504 binary_expr.right = call.clone();
6505 }
6506 (false, false) => return TraversalReturn::new_continue(()),
6507 }
6508
6509 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6510 }
6511 }
6512 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6513 if let NodeMut::BinaryExpression(binary_expr) = node {
6514 let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6515 call
6516 } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6517 call
6518 } else {
6519 return TraversalReturn::new_continue(());
6520 };
6521
6522 if let Some(label_arg) = call
6523 .arguments
6524 .iter_mut()
6525 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6526 {
6527 label_arg.arg = label_position.clone();
6528 } else {
6529 call.arguments.push(ast::LabeledArg {
6530 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6531 arg: label_position.clone(),
6532 });
6533 }
6534
6535 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6536 }
6537 }
6538 AstMutateCommand::EditCallUnlabeled { arg } => {
6539 if let NodeMut::CallExpressionKw(call) = node {
6540 call.unlabeled = Some(arg.clone());
6541 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6542 }
6543 }
6544 AstMutateCommand::EditVarInitialValue { value } => {
6545 if let NodeMut::SketchVar(sketch_var) = node {
6549 let Ok(literal) = to_source_number(*value) else {
6550 return TraversalReturn::new_break(Err(KclError::refactor(format!(
6551 "Could not convert number to AST literal: {:?}",
6552 *value
6553 ))));
6554 };
6555 sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6556 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6557 }
6558 }
6559 AstMutateCommand::DeleteNode => {
6560 return TraversalReturn {
6561 mutate_body_item: MutateBodyItem::Delete,
6562 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6563 };
6564 }
6565 }
6566 TraversalReturn::new_continue(())
6567}
6568
6569struct FindSketchBlockSourceRange {
6570 target_before_mutation: SourceRange,
6572 found: Cell<Option<AstNodeRef>>,
6576}
6577
6578impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6579 type Error = crate::front::Error;
6580
6581 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6582 let Ok(node_range) = SourceRange::try_from(&node) else {
6583 return Ok(true);
6584 };
6585
6586 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6587 if node_range.module_id() == self.target_before_mutation.module_id()
6588 && node_range.start() == self.target_before_mutation.start()
6589 && node_range.end() >= self.target_before_mutation.end()
6591 {
6592 self.found.set(sketch_block.body.items.last().map(|item| match item {
6593 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6597 _ => AstNodeRef::from(item),
6598 }));
6599 return Ok(false);
6600 } else {
6601 return Ok(true);
6604 }
6605 }
6606
6607 for child in node.children().iter() {
6608 if !child.visit(*self)? {
6609 return Ok(false);
6610 }
6611 }
6612
6613 Ok(true)
6614 }
6615}
6616
6617struct FindSketchBlockByNodePath {
6618 target_node_path: ast::NodePath,
6620 found: Cell<Option<AstNodeRef>>,
6624}
6625
6626impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6627 type Error = crate::front::Error;
6628
6629 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6630 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6631 return Ok(true);
6632 };
6633
6634 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6635 if let Some(node_path) = node_path
6636 && node_path == self.target_node_path
6637 {
6638 self.found.set(sketch_block.body.items.last().map(|item| match item {
6639 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6643 _ => AstNodeRef::from(item),
6644 }));
6645
6646 return Ok(false);
6647 } else {
6648 return Ok(true);
6651 }
6652 }
6653
6654 for child in node.children().iter() {
6655 if !child.visit(*self)? {
6656 return Ok(false);
6657 }
6658 }
6659
6660 Ok(true)
6661 }
6662}
6663
6664fn find_sketch_block_added_item(
6672 ast: &ast::Node<ast::Program>,
6673 sketch_block_before_mutation: &AstNodeRef,
6674) -> Result<AstNodeRef, KclError> {
6675 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6676 let find = FindSketchBlockByNodePath {
6677 target_node_path: node_path.clone(),
6678 found: Cell::new(None),
6679 };
6680 let node = crate::walk::Node::from(ast);
6681 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6682 find.found.into_inner().ok_or_else(|| {
6683 KclError::refactor(format!(
6684 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6685 ))
6686 })
6687 } else {
6688 let find = FindSketchBlockSourceRange {
6690 target_before_mutation: sketch_block_before_mutation.range,
6691 found: Cell::new(None),
6692 };
6693 let node = crate::walk::Node::from(ast);
6694 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6695 find.found.into_inner().ok_or_else(|| KclError::refactor(
6696 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?"),
6697 ))
6698 }
6699}
6700
6701fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6702 let message = error.message().trim();
6703 let message = if message.is_empty() {
6704 "unknown parse error"
6705 } else {
6706 message
6707 };
6708
6709 format!("{prefix}: {message}")
6710}
6711
6712fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6713 let (program, errors) = Program::parse(source).map_err(|err| {
6714 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6715 })?;
6716 if !errors.is_empty() {
6717 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6718 format_compilation_issues(parse_error_prefix, &errors),
6719 )));
6720 }
6721
6722 program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6723}
6724
6725fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6726 let Some(first_issue) = issues
6727 .iter()
6728 .find(|issue| issue.severity.is_err())
6729 .or_else(|| issues.first())
6730 else {
6731 return prefix.to_owned();
6732 };
6733
6734 let message = first_issue.message.trim();
6735 let message = if message.is_empty() {
6736 "unknown parse error"
6737 } else {
6738 message
6739 };
6740
6741 if issues.len() > 1 {
6742 format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6743 } else {
6744 format!("{prefix}: {message}")
6745 }
6746}
6747
6748fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6749 ast.recast_top(&Default::default(), 0)
6751}
6752
6753struct FindNumericLiteral {
6754 target: SourceRange,
6755 found: Cell<Option<ast::NumericLiteral>>,
6756}
6757
6758impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6759 type Error = crate::front::Error;
6760
6761 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6762 let Ok(node_range) = SourceRange::try_from(&node) else {
6763 return Ok(true);
6764 };
6765
6766 if node_range == self.target
6767 && let crate::walk::Node::NumericLiteral(literal) = node
6768 {
6769 self.found.set(Some(literal.inner.clone()));
6770 return Ok(false);
6771 }
6772
6773 for child in node.children().iter() {
6774 if !child.visit(*self)? {
6775 return Ok(false);
6776 }
6777 }
6778
6779 Ok(true)
6780 }
6781}
6782
6783fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6784 let find = FindNumericLiteral {
6785 target,
6786 found: Cell::new(None),
6787 };
6788 let node = crate::walk::Node::from(ast);
6789 node.visit(&find).ok()?;
6790 find.found.into_inner()
6791}
6792
6793struct FindSketchVarInitialByNodePath<'a> {
6794 target: &'a ast::NodePath,
6795 sketch_var_found: Cell<bool>,
6796 initial_literal: Cell<Option<ast::NumericLiteral>>,
6797}
6798
6799impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6800 type Error = crate::front::Error;
6801
6802 fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6803 if let crate::walk::Node::SketchVar(sketch_var) = node
6804 && sketch_var.node_path.as_ref() == Some(self.target)
6805 {
6806 self.sketch_var_found.set(true);
6807 if let Some(initial) = &sketch_var.initial {
6808 self.initial_literal.set(Some(initial.inner.clone()));
6809 }
6810 return Ok(false);
6811 }
6812
6813 for child in node.children().iter() {
6814 if !child.visit(*self)? {
6815 return Ok(false);
6816 }
6817 }
6818
6819 Ok(true)
6820 }
6821}
6822
6823fn numeric_literal_at_node_path(
6833 ast: &ast::Node<ast::Program>,
6834 node_path: Option<&ast::NodePath>,
6835 source_range: SourceRange,
6836) -> Option<Option<ast::NumericLiteral>> {
6837 let Some(node_path) = node_path else {
6838 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";
6839 #[cfg(target_arch = "wasm32")]
6840 web_sys::console::warn_1(&message.into());
6841 #[cfg(not(target_arch = "wasm32"))]
6842 eprintln!("WARNING: {message}");
6843 return numeric_literal_at_source_range(ast, source_range).map(Some);
6844 };
6845 let find = FindSketchVarInitialByNodePath {
6846 target: node_path,
6847 sketch_var_found: Cell::new(false),
6848 initial_literal: Cell::new(None),
6849 };
6850 let node = crate::walk::Node::from(ast);
6851 node.visit(&find).ok()?;
6852 if !find.sketch_var_found.get() {
6853 return None;
6854 }
6855 Some(find.initial_literal.into_inner())
6856}
6857
6858fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6859 match suffix {
6860 NumericSuffix::Mm => Some(UnitLength::Millimeters),
6861 NumericSuffix::Cm => Some(UnitLength::Centimeters),
6862 NumericSuffix::M => Some(UnitLength::Meters),
6863 NumericSuffix::Inch => Some(UnitLength::Inches),
6864 NumericSuffix::Ft => Some(UnitLength::Feet),
6865 NumericSuffix::Yd => Some(UnitLength::Yards),
6866 _ => None,
6867 }
6868}
6869
6870fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6871 match suffix_length_unit(number.units) {
6872 Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6873 None => number.value,
6874 }
6875}
6876
6877fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6878 match suffix_length_unit(literal.suffix) {
6879 Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6880 None => literal.value,
6881 }
6882}
6883
6884fn var_solution_needs_commit(
6885 current_literal: &ast::NumericLiteral,
6886 solved_value: Number,
6887 default_length_unit: UnitLength,
6888) -> bool {
6889 let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6890 let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6891
6892 (current - solved).abs() > 1e-9
6893}
6894
6895fn preserve_var_solution_literal_style(
6896 current_literal: &ast::NumericLiteral,
6897 solved_value: Number,
6898 default_length_unit: UnitLength,
6899) -> Number {
6900 if current_literal.suffix == NumericSuffix::None {
6901 return Number {
6902 value: number_value_in_default_length_units(solved_value, default_length_unit),
6903 units: NumericSuffix::None,
6904 };
6905 }
6906
6907 let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6908 return solved_value;
6909 };
6910
6911 let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6912 Number {
6913 value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6914 units: current_literal.suffix,
6915 }
6916}
6917
6918pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6919 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6920 inner: ast::ArrayExpression {
6921 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6922 non_code_meta: Default::default(),
6923 digest: None,
6924 },
6925 start: Default::default(),
6926 end: Default::default(),
6927 module_id: Default::default(),
6928 node_path: None,
6929 outer_attrs: Default::default(),
6930 pre_comments: Default::default(),
6931 comment_start: Default::default(),
6932 })))
6933}
6934
6935pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6936 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6937 ast::ArrayExpression {
6938 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6939 digest: None,
6940 non_code_meta: Default::default(),
6941 },
6942 ))))
6943}
6944
6945fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6946 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6947 ast::ArrayExpression {
6948 elements: vec![
6949 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6950 point.x,
6951 )?)))),
6952 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6953 point.y,
6954 )?)))),
6955 ],
6956 non_code_meta: Default::default(),
6957 digest: None,
6958 },
6959 ))))
6960}
6961
6962fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6963 match expr {
6964 Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6965 inner: ast::Literal::from(to_source_number(*number)?),
6966 start: Default::default(),
6967 end: Default::default(),
6968 module_id: Default::default(),
6969 node_path: None,
6970 outer_attrs: Default::default(),
6971 pre_comments: Default::default(),
6972 comment_start: Default::default(),
6973 }))),
6974 Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6975 inner: ast::SketchVar {
6976 initial: Some(BoxNode::new(ast::Node {
6977 inner: to_source_number(*number)?,
6978 start: Default::default(),
6979 end: Default::default(),
6980 module_id: Default::default(),
6981 node_path: None,
6982 outer_attrs: Default::default(),
6983 pre_comments: Default::default(),
6984 comment_start: Default::default(),
6985 })),
6986 digest: None,
6987 },
6988 start: Default::default(),
6989 end: Default::default(),
6990 module_id: Default::default(),
6991 node_path: None,
6992 outer_attrs: Default::default(),
6993 pre_comments: Default::default(),
6994 comment_start: Default::default(),
6995 }))),
6996 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6997 }
6998}
6999
7000fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
7001 Ok(ast::NumericLiteral {
7002 value: number.value,
7003 suffix: number.units,
7004 raw: format_number_literal(number.value, number.units, None)?,
7005 digest: None,
7006 })
7007}
7008
7009pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7010 ast::Expr::Name(BoxNode::new(ast_name(name)))
7011}
7012
7013fn ast_name(name: String) -> ast::Node<ast::Name> {
7014 ast::Node {
7015 inner: ast::Name {
7016 name: ast::Node {
7017 inner: ast::Identifier { name, digest: None },
7018 start: Default::default(),
7019 end: Default::default(),
7020 module_id: Default::default(),
7021 node_path: None,
7022 outer_attrs: Default::default(),
7023 pre_comments: Default::default(),
7024 comment_start: Default::default(),
7025 },
7026 path: Vec::new(),
7027 abs_path: false,
7028 digest: None,
7029 },
7030 start: Default::default(),
7031 end: Default::default(),
7032 module_id: Default::default(),
7033 node_path: None,
7034 outer_attrs: Default::default(),
7035 pre_comments: Default::default(),
7036 comment_start: Default::default(),
7037 }
7038}
7039
7040pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7041 ast::Name {
7042 name: ast::Node {
7043 inner: ast::Identifier {
7044 name: name.to_owned(),
7045 digest: None,
7046 },
7047 start: Default::default(),
7048 end: Default::default(),
7049 module_id: Default::default(),
7050 node_path: None,
7051 outer_attrs: Default::default(),
7052 pre_comments: Default::default(),
7053 comment_start: Default::default(),
7054 },
7055 path: Default::default(),
7056 abs_path: false,
7057 digest: None,
7058 }
7059}
7060
7061pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
7063 let elements = exprs.into_iter().collect::<Vec<_>>();
7064 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
7065
7066 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7068 elements,
7069 digest: None,
7070 non_code_meta: Default::default(),
7071 })));
7072
7073 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7075 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
7076 unlabeled: Some(array_expr),
7077 arguments: Default::default(),
7078 digest: None,
7079 non_code_meta: Default::default(),
7080 })))
7081}
7082
7083pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7085 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7086 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7087 unlabeled: Some(line_expr),
7088 arguments: Default::default(),
7089 digest: None,
7090 non_code_meta: Default::default(),
7091 })))
7092}
7093
7094pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7096 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7097 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7098 unlabeled: Some(line_expr),
7099 arguments: Default::default(),
7100 digest: None,
7101 non_code_meta: Default::default(),
7102 })))
7103}
7104
7105pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7107 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7108 object: object_expr,
7109 property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7110 name: ast::Node::no_src(ast::Identifier {
7111 name: property.to_string(),
7112 digest: None,
7113 }),
7114 path: Vec::new(),
7115 abs_path: false,
7116 digest: None,
7117 }))),
7118 computed: false,
7119 digest: None,
7120 })))
7121}
7122
7123pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7124 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7125 object: object_expr,
7126 property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7127 ast::NumericLiteral {
7128 value: index as f64,
7129 suffix: NumericSuffix::None,
7130 raw: index.to_string(),
7131 digest: None,
7132 },
7133 )))),
7134 computed: true,
7135 digest: None,
7136 })))
7137}
7138
7139fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7141 let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7143 position.x,
7144 )?))));
7145 let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7146 position.y,
7147 )?))));
7148 let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7149 elements: vec![x_literal, y_literal],
7150 digest: None,
7151 non_code_meta: Default::default(),
7152 })));
7153
7154 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7156 elements: vec![point_expr, point_array],
7157 digest: None,
7158 non_code_meta: Default::default(),
7159 })));
7160
7161 Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7163 ast::CallExpressionKw {
7164 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7165 unlabeled: Some(array_expr),
7166 arguments: Default::default(),
7167 digest: None,
7168 non_code_meta: Default::default(),
7169 },
7170 ))))
7171}
7172
7173pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7175 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7176 elements: line_exprs,
7177 digest: None,
7178 non_code_meta: Default::default(),
7179 })));
7180
7181 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7183 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7184 unlabeled: Some(array_expr),
7185 arguments: Default::default(),
7186 digest: None,
7187 non_code_meta: Default::default(),
7188 })))
7189}
7190
7191pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7193 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7194 elements: segment_exprs,
7195 digest: None,
7196 non_code_meta: Default::default(),
7197 })));
7198
7199 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7200 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7201 unlabeled: Some(array_expr),
7202 arguments: Default::default(),
7203 digest: None,
7204 non_code_meta: Default::default(),
7205 })))
7206}
7207
7208pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7210 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7211 elements: vec![seg1_expr, seg2_expr],
7212 digest: None,
7213 non_code_meta: Default::default(),
7214 })));
7215
7216 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7217 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7218 unlabeled: Some(array_expr),
7219 arguments: Default::default(),
7220 digest: None,
7221 non_code_meta: Default::default(),
7222 })))
7223}
7224
7225pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7227 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7228 elements: input_exprs,
7229 digest: None,
7230 non_code_meta: Default::default(),
7231 })));
7232 let arguments = vec![ast::LabeledArg {
7233 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7234 arg: axis_expr,
7235 }];
7236
7237 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7238 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7239 unlabeled: Some(array_expr),
7240 arguments,
7241 digest: None,
7242 non_code_meta: Default::default(),
7243 })))
7244}
7245
7246pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7248 let arguments = vec![ast::LabeledArg {
7249 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7250 arg: point_expr,
7251 }];
7252
7253 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7254 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7255 unlabeled: Some(segment_expr),
7256 arguments,
7257 digest: None,
7258 non_code_meta: Default::default(),
7259 })))
7260}
7261
7262fn issue_source_range(error: &KclError) -> SourceRange {
7267 let source_ranges = error.source_ranges();
7268 source_ranges
7269 .iter()
7270 .find(|range| range.is_top_level_module())
7271 .or_else(|| source_ranges.first())
7272 .copied()
7273 .unwrap_or_else(SourceRange::synthetic)
7274}
7275
7276#[cfg(test)]
7277mod tests {
7278 use std::sync;
7279
7280 use super::*;
7281 use crate::engine::PlaneName;
7282 use crate::engine::engine_manager::EngineManager;
7283 use crate::execution::cache::SketchModeState;
7284 use crate::execution::cache::clear_mem_cache;
7285 use crate::execution::cache::read_old_memory;
7286 use crate::execution::cache::write_old_memory;
7287 use crate::front::Distance;
7288 use crate::front::Fixed;
7289 use crate::front::FixedPoint;
7290 use crate::front::Midpoint;
7291 use crate::front::Object;
7292 use crate::front::Plane;
7293 use crate::front::Sketch;
7294 use crate::front::Tangent;
7295 use crate::frontend::sketch::Vertical;
7296 use crate::pretty::NumericSuffix;
7297
7298 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7299 for object in &scene_graph.objects {
7300 if let ObjectKind::Sketch(_) = &object.kind {
7301 return Some(object);
7302 }
7303 }
7304 None
7305 }
7306
7307 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7308 for object in &scene_graph.objects {
7309 if let ObjectKind::Face(_) = &object.kind {
7310 return Some(object);
7311 }
7312 }
7313 None
7314 }
7315
7316 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7317 for object in &scene_graph.objects {
7318 if matches!(&object.kind, ObjectKind::Wall(_)) {
7319 return Some(object.id);
7320 }
7321 }
7322 None
7323 }
7324
7325 fn find_cap_object_id_with_solid_output_index(
7326 scene_graph: &SceneGraph,
7327 cap_kind: crate::frontend::api::CapKind,
7328 solid_output_index: usize,
7329 ) -> Option<ObjectId> {
7330 for object in &scene_graph.objects {
7331 if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7332 {
7333 return Some(object.id);
7334 }
7335 }
7336 None
7337 }
7338
7339 #[test]
7340 fn issue_source_range_prefers_top_level_module() {
7341 use kcl_error::ModuleId;
7342
7343 let top = SourceRange::new(10, 20, ModuleId::default());
7344 let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7345
7346 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7349 "boom".to_owned(),
7350 vec![imported, top],
7351 ));
7352 assert_eq!(super::issue_source_range(&error), top);
7353
7354 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7356 assert_eq!(super::issue_source_range(&error), imported);
7357
7358 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7360 assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7361 }
7362
7363 #[test]
7364 fn composite_constituent_sweeps_are_not_solid_outputs() {
7365 use kcl_api::artifact::ArtifactSweepMethod;
7366 use kcl_api::artifact::CompositeSolid;
7367 use kcl_api::artifact::CompositeSolidSubType;
7368 use kcl_api::artifact::Sweep;
7369 use kcl_api::artifact::SweepSubType;
7370
7371 let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7372 let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7373 let composite_id = ArtifactId::new(Uuid::new_v4());
7374 let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7375 let sweep = |id| {
7376 Artifact::Sweep(Sweep {
7377 id,
7378 sub_type: SweepSubType::Extrusion,
7379 path_id: ArtifactId::new(Uuid::new_v4()),
7380 surface_ids: Vec::new(),
7381 edge_ids: Vec::new(),
7382 code_ref: code_ref.clone(),
7383 source_sweep_id: None,
7384 trajectory_id: None,
7385 method: ArtifactSweepMethod::New,
7386 consumed: false,
7387 pattern_ids: Vec::new(),
7388 })
7389 };
7390 let mut artifacts = IndexMap::from([
7391 (first_sweep_id, sweep(first_sweep_id)),
7392 (second_sweep_id, sweep(second_sweep_id)),
7393 ]);
7394
7395 let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7396 assert_eq!(
7397 solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7398 Some(0)
7399 );
7400 assert_eq!(
7401 solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7402 Some(1)
7403 );
7404
7405 artifacts.insert(
7406 composite_id,
7407 Artifact::CompositeSolid(CompositeSolid {
7408 id: composite_id,
7409 consumed: false,
7410 sub_type: CompositeSolidSubType::Union,
7411 output_index: None,
7412 solid_ids: vec![first_sweep_id, second_sweep_id],
7413 tool_ids: Vec::new(),
7414 code_ref,
7415 composite_solid_id: None,
7416 pattern_ids: Vec::new(),
7417 }),
7418 );
7419 let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7420 assert_eq!(
7421 solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7422 None
7423 );
7424 assert_eq!(
7425 solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7426 None
7427 );
7428 }
7429
7430 #[test]
7431 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7432 let source = "\
7433region001 = region(point = [0.1, 0.1], sketch = s)
7434extrude001 = extrude(region001, length = 5)
7435revolve001 = revolve(region001, axis = Y)
7436sweep001 = sweep(region001, path = path001)
7437loft001 = loft(region001)
7438not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7439";
7440
7441 let program = Program::parse(source).unwrap().0.unwrap();
7442
7443 assert_eq!(
7444 region_name_from_sweep_variable(&program.ast, "extrude001"),
7445 Some("region001".to_owned())
7446 );
7447 assert_eq!(
7448 region_name_from_sweep_variable(&program.ast, "revolve001"),
7449 Some("region001".to_owned())
7450 );
7451 assert_eq!(
7452 region_name_from_sweep_variable(&program.ast, "sweep001"),
7453 Some("region001".to_owned())
7454 );
7455 assert_eq!(
7456 region_name_from_sweep_variable(&program.ast, "loft001"),
7457 Some("region001".to_owned())
7458 );
7459 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7460 }
7461
7462 #[track_caller]
7463 fn expect_sketch(object: &Object) -> &Sketch {
7464 if let ObjectKind::Sketch(sketch) = &object.kind {
7465 sketch
7466 } else {
7467 panic!("Object is not a sketch: {:?}", object);
7468 }
7469 }
7470
7471 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7472 let point_object = scene_graph.objects.get(point_id.0).unwrap();
7473 let ObjectKind::Segment {
7474 segment: Segment::Point(point),
7475 } = &point_object.kind
7476 else {
7477 panic!("Object is not a point segment: {point_object:?}");
7478 };
7479 point.position.clone()
7480 }
7481
7482 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7483 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7484 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7485 }
7486
7487 fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7490 Point2d {
7491 x: Expr::Var(Number {
7492 value: x,
7493 units: NumericSuffix::Mm,
7494 }),
7495 y: Expr::Var(Number {
7496 value: y,
7497 units: NumericSuffix::Mm,
7498 }),
7499 }
7500 }
7501
7502 fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7505 Point2d {
7506 x: Number {
7507 value: x,
7508 units: NumericSuffix::Mm,
7509 },
7510 y: Number {
7511 value: y,
7512 units: NumericSuffix::Mm,
7513 },
7514 }
7515 }
7516
7517 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7518 LineCtor {
7519 start: Point2d {
7520 x: Expr::Number(Number { value: start_x, units }),
7521 y: Expr::Number(Number { value: start_y, units }),
7522 },
7523 end: Point2d {
7524 x: Expr::Number(Number { value: end_x, units }),
7525 y: Expr::Number(Number { value: end_y, units }),
7526 },
7527 construction: None,
7528 }
7529 }
7530
7531 async fn create_sketch_with_single_line(
7532 frontend: &mut FrontendState,
7533 ctx: &ExecutorContext,
7534 mock_ctx: &ExecutorContext,
7535 version: Version,
7536 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7537 frontend.program = Program::empty();
7538
7539 let sketch_args = SketchCtor {
7540 on: Plane::Default(PlaneName::Xy),
7541 };
7542 let (_src_delta, _scene_delta, sketch_id) = frontend
7543 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7544 .await
7545 .unwrap();
7546
7547 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7548 let (source_delta, scene_graph_delta) = frontend
7549 .add_segment(mock_ctx, version, sketch_id, segment, None)
7550 .await
7551 .unwrap();
7552 let line_id = *scene_graph_delta
7553 .new_objects
7554 .last()
7555 .expect("Expected line object id to be created");
7556
7557 (sketch_id, line_id, source_delta, scene_graph_delta)
7558 }
7559
7560 async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7561 frontend.program = program.clone();
7562 let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7563 frontend.update_state_after_exec(outcome, true);
7564 }
7565
7566 #[test]
7567 fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7568 for (source, expected_message) in [
7569 ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7570 (
7571 "3'",
7572 "Error parsing KCL source after editing: unterminated string literal",
7573 ),
7574 ] {
7575 let err = parse_frontend_mutation_source(
7576 source,
7577 "Error parsing KCL source after editing",
7578 "No AST produced after editing",
7579 )
7580 .expect_err("expected invalid KCL source to fail");
7581 let message = err.error.message();
7582
7583 assert_eq!(message, expected_message);
7584 assert!(!message.contains("CompilationIssue"));
7585 assert!(!message.contains("KclErrorDetails"));
7586 assert!(!message.contains("source_range"));
7587 }
7588 }
7589
7590 #[tokio::test(flavor = "multi_thread")]
7591 async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7592 let initial_source = "\
7593sketch(on = XY) {
7594 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7595 distance([line1.start, line1.end]) == 10
7596}
7597";
7598 let program = Program::parse(initial_source).unwrap().0.unwrap();
7599
7600 let mut frontend = FrontendState::new();
7601 let mock_ctx = ExecutorContext::new_mock(None).await;
7602 let version = Version(0);
7603
7604 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7605 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7606 let sketch_id = sketch_object.id;
7607 let sketch = expect_sketch(sketch_object);
7608 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7609
7610 for (value, expected_message) in [
7611 ("**", "Invalid constraint value: Unexpected token: *"),
7612 ("3'", "Invalid constraint value: unterminated string literal"),
7613 ] {
7614 let err = frontend
7615 .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7616 .await
7617 .expect_err("expected invalid constraint expression to fail");
7618 let message = err.error.message();
7619
7620 assert_eq!(message, expected_message);
7621 assert!(!message.contains("CompilationIssue"));
7622 assert!(!message.contains("KclErrorDetails"));
7623 assert!(!message.contains("source_range"));
7624 }
7625
7626 mock_ctx.close().await;
7627 }
7628
7629 #[tokio::test(flavor = "multi_thread")]
7630 async fn test_failed_edit_constraint_value_does_not_update_program() {
7631 let initial_source = "\
7632sketch(on = XY) {
7633 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7634 distance([line1.start, line1.end]) == 10
7635}
7636";
7637 let program = Program::parse(initial_source).unwrap().0.unwrap();
7638 let original_source = program.original_file_contents.clone();
7639
7640 let mut frontend = FrontendState::new();
7641 let mock_ctx = ExecutorContext::new_mock(None).await;
7642 let version = Version(0);
7643
7644 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7645 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7646 let sketch_id = sketch_object.id;
7647 let sketch = expect_sketch(sketch_object);
7648 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7649
7650 frontend
7651 .edit_constraint_value(
7652 &mock_ctx,
7653 version,
7654 sketch_id,
7655 constraint_id,
7656 "unknownDistance".to_owned(),
7657 )
7658 .await
7659 .expect_err("expected invalid constraint value to fail execution");
7660
7661 assert_eq!(frontend.program.original_file_contents, original_source);
7662 assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7663
7664 mock_ctx.close().await;
7665 }
7666
7667 #[tokio::test(flavor = "multi_thread")]
7668 async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7669 let initial_source = "\
7670arr = [0]
7671sketch(on = XY) {
7672 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7673 distance([line1.start, line1.end]) == 10
7674}
7675";
7676 let program = Program::parse(initial_source).unwrap().0.unwrap();
7677
7678 let mut frontend = FrontendState::new();
7679 let mock_ctx = ExecutorContext::new_mock(None).await;
7680 let version = Version(0);
7681
7682 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7683 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7684 let sketch_id = sketch_object.id;
7685 let sketch = expect_sketch(sketch_object);
7686 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7687
7688 let err = frontend
7692 .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7693 .await
7694 .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7695 let message = err.error.message();
7696 assert!(
7697 message.contains("The array doesn't have any item at index 5"),
7698 "unexpected error message: {message}"
7699 );
7700
7701 mock_ctx.close().await;
7702 }
7703
7704 #[tokio::test(flavor = "multi_thread")]
7705 async fn test_sketch_checkpoint_round_trip_restores_state() {
7706 let mut frontend = FrontendState::new();
7707 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7708 let mock_ctx = ExecutorContext::new_mock(None).await;
7709 let version = Version(0);
7710
7711 let (sketch_id, line_id, source_delta, scene_graph_delta) =
7712 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7713
7714 let expected_source = source_delta.text.clone();
7715 let expected_scene_graph = frontend.scene_graph.clone();
7716 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7717 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7718
7719 let checkpoint_id = frontend
7720 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7721 .await
7722 .unwrap();
7723
7724 let edited_segments = vec![ExistingSegmentCtor {
7725 id: line_id,
7726 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7727 }];
7728 let (edited_source, _edited_scene) = frontend
7729 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7730 .await
7731 .unwrap();
7732 assert_ne!(edited_source.text, expected_source);
7733
7734 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7735
7736 assert_eq!(restored.source_delta.text, expected_source);
7737 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7738 assert!(restored.scene_graph_delta.invalidates_ids);
7739 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7740 assert_eq!(frontend.scene_graph, expected_scene_graph);
7741 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7742
7743 ctx.close().await;
7744 }
7745
7746 #[tokio::test(flavor = "multi_thread")]
7747 async fn test_sketch_checkpoints_prune_oldest_entries() {
7748 let mut frontend = FrontendState::new();
7749 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7750 let mock_ctx = ExecutorContext::new_mock(None).await;
7751 let version = Version(0);
7752
7753 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7754 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7755
7756 let mut checkpoint_ids = Vec::new();
7757 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7758 checkpoint_ids.push(
7759 frontend
7760 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7761 .await
7762 .unwrap(),
7763 );
7764 }
7765
7766 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7767 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7768
7769 let oldest_retained = checkpoint_ids[3];
7770 assert_eq!(
7771 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7772 Some(oldest_retained)
7773 );
7774
7775 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7776 assert!(evicted_restore.is_err());
7777 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7778
7779 frontend
7780 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7781 .await
7782 .unwrap();
7783
7784 ctx.close().await;
7785 }
7786
7787 #[tokio::test(flavor = "multi_thread")]
7788 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7789 let mut frontend = FrontendState::new();
7790 let missing_checkpoint = SketchCheckpointId::new(999);
7791
7792 let err = frontend
7793 .restore_sketch_checkpoint(missing_checkpoint)
7794 .await
7795 .expect_err("Expected restore to fail for missing checkpoint");
7796
7797 assert!(err.msg.contains("Sketch checkpoint not found"));
7798 }
7799
7800 #[tokio::test(flavor = "multi_thread")]
7801 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7802 let mut frontend = FrontendState::new();
7803 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7804 let mock_ctx = ExecutorContext::new_mock(None).await;
7805 let version = Version(0);
7806
7807 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7808 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7809
7810 let checkpoint_a = frontend
7811 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7812 .await
7813 .unwrap();
7814 let checkpoint_b = frontend
7815 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7816 .await
7817 .unwrap();
7818 assert_eq!(frontend.sketch_checkpoints.len(), 2);
7819
7820 frontend.clear_sketch_checkpoints();
7821 assert!(frontend.sketch_checkpoints.is_empty());
7822 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7823 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7824
7825 ctx.close().await;
7826 }
7827
7828 #[tokio::test(flavor = "multi_thread")]
7829 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7830 let mut frontend = FrontendState::new();
7831 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7832 let mock_ctx = ExecutorContext::new_mock(None).await;
7833 let version = Version(0);
7834
7835 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7836 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7837 let old_source = source_delta.text.clone();
7838 let old_checkpoint = frontend
7839 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7840 .await
7841 .unwrap();
7842 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7843
7844 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
7845 .unwrap()
7846 .0
7847 .unwrap();
7848
7849 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7850 let SetProgramOutcome::Success {
7851 checkpoint_id: Some(new_checkpoint),
7852 ..
7853 } = result
7854 else {
7855 panic!("Expected Success with a fresh checkpoint baseline");
7856 };
7857
7858 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7859
7860 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7861 assert_eq!(old_restore.source_delta.text, old_source);
7862
7863 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7864 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7865
7866 ctx.close().await;
7867 }
7868
7869 #[tokio::test(flavor = "multi_thread")]
7870 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7871 let mut frontend = FrontendState::new();
7872 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7873 let mock_ctx = ExecutorContext::new_mock(None).await;
7874 let version = Version(0);
7875
7876 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7877 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7878 let old_checkpoint = frontend
7879 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7880 .await
7881 .unwrap();
7882 let checkpoint_count_before = frontend.sketch_checkpoints.len();
7883
7884 let failing_program = Program::parse(
7885 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7886 )
7887 .unwrap()
7888 .0
7889 .unwrap();
7890
7891 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7892 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7893 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7894 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7895
7896 ctx.close().await;
7897 }
7898
7899 #[tokio::test(flavor = "multi_thread")]
7900 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7901 let mut frontend = FrontendState::new();
7902 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7903
7904 let program = Program::parse(
7905 "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",
7906 )
7907 .unwrap()
7908 .0
7909 .unwrap();
7910 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7911 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7912 panic!("Expected successful baseline program execution");
7913 };
7914
7915 clear_mem_cache().await;
7916 assert!(read_old_memory().await.is_none());
7917
7918 let checkpoint_without_mock_memory = frontend
7919 .create_sketch_checkpoint((*exec_outcome).clone())
7920 .await
7921 .unwrap();
7922
7923 write_old_memory(SketchModeState::new_for_tests()).await;
7924 assert!(read_old_memory().await.is_some());
7925
7926 let checkpoint_with_mock_memory = frontend
7927 .create_sketch_checkpoint((*exec_outcome).clone())
7928 .await
7929 .unwrap();
7930
7931 clear_mem_cache().await;
7932 assert!(read_old_memory().await.is_none());
7933
7934 frontend
7935 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7936 .await
7937 .unwrap();
7938 assert!(read_old_memory().await.is_some());
7939
7940 frontend
7941 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7942 .await
7943 .unwrap();
7944 assert!(read_old_memory().await.is_none());
7945
7946 ctx.close().await;
7947 }
7948
7949 #[tokio::test(flavor = "multi_thread")]
7950 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7951 let source = "\
7952sketch(on = XY) {
7953 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7954}
7955
7956bad = missing_name
7957";
7958 let program = Program::parse(source).unwrap().0.unwrap();
7959
7960 let mut frontend = FrontendState::new();
7961
7962 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7963 let mock_ctx = ExecutorContext::new_mock(None).await;
7964 let version = Version(0);
7965 let project_id = ProjectId(0);
7966 let file_id = FileId(0);
7967
7968 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7969 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7970 };
7971
7972 let sketch_id = frontend
7973 .scene_graph
7974 .objects
7975 .iter()
7976 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7977 .expect("Expected sketch object from errored hack_set_program");
7978
7979 frontend
7980 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7981 .await
7982 .unwrap();
7983
7984 ctx.close().await;
7985 mock_ctx.close().await;
7986 }
7987
7988 #[tokio::test(flavor = "multi_thread")]
7989 async fn test_new_sketch_add_point_edit_point() {
7990 let program = Program::empty();
7991
7992 let mut frontend = FrontendState::new();
7993 frontend.program = program;
7994
7995 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7996 let mock_ctx = ExecutorContext::new_mock(None).await;
7997 let version = Version(0);
7998
7999 let sketch_args = SketchCtor {
8000 on: Plane::Default(PlaneName::Xy),
8001 };
8002 let (_src_delta, scene_delta, sketch_id) = frontend
8003 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8004 .await
8005 .unwrap();
8006 assert_eq!(sketch_id, ObjectId(1));
8007 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8008 let sketch_object = &scene_delta.new_graph.objects[1];
8009 assert_eq!(sketch_object.id, ObjectId(1));
8010 assert_eq!(
8011 sketch_object.kind,
8012 ObjectKind::Sketch(Sketch {
8013 args: SketchCtor {
8014 on: Plane::Default(PlaneName::Xy)
8015 },
8016 plane: ObjectId(0),
8017 segments: vec![],
8018 constraints: vec![],
8019 })
8020 );
8021 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8022
8023 let point_ctor = PointCtor {
8024 position: Point2d {
8025 x: Expr::Number(Number {
8026 value: 1.0,
8027 units: NumericSuffix::Inch,
8028 }),
8029 y: Expr::Number(Number {
8030 value: 2.0,
8031 units: NumericSuffix::Inch,
8032 }),
8033 },
8034 };
8035 let segment = SegmentCtor::Point(point_ctor);
8036 let (src_delta, scene_delta) = frontend
8037 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8038 .await
8039 .unwrap();
8040 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8041 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8042 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8043 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8044 assert_eq!(scene_object.id.0, i);
8045 }
8046
8047 let point_id = *scene_delta.new_objects.last().unwrap();
8048
8049 let point_ctor = PointCtor {
8050 position: Point2d {
8051 x: Expr::Number(Number {
8052 value: 3.0,
8053 units: NumericSuffix::Inch,
8054 }),
8055 y: Expr::Number(Number {
8056 value: 4.0,
8057 units: NumericSuffix::Inch,
8058 }),
8059 },
8060 };
8061 let segments = vec![ExistingSegmentCtor {
8062 id: point_id,
8063 ctor: SegmentCtor::Point(point_ctor),
8064 }];
8065 let (src_delta, scene_delta) = frontend
8066 .edit_segments(&mock_ctx, version, sketch_id, segments)
8067 .await
8068 .unwrap();
8069 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8070 assert_eq!(scene_delta.new_objects, vec![]);
8071 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8072
8073 ctx.close().await;
8074 mock_ctx.close().await;
8075 }
8076
8077 #[tokio::test(flavor = "multi_thread")]
8078 async fn test_new_sketch_add_line_edit_line() {
8079 let program = Program::empty();
8080
8081 let mut frontend = FrontendState::new();
8082 frontend.program = program;
8083
8084 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8085 let mock_ctx = ExecutorContext::new_mock(None).await;
8086 let version = Version(0);
8087
8088 let sketch_args = SketchCtor {
8089 on: Plane::Default(PlaneName::Xy),
8090 };
8091 let (_src_delta, scene_delta, sketch_id) = frontend
8092 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8093 .await
8094 .unwrap();
8095 assert_eq!(sketch_id, ObjectId(1));
8096 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8097 let sketch_object = &scene_delta.new_graph.objects[1];
8098 assert_eq!(sketch_object.id, ObjectId(1));
8099 assert_eq!(
8100 sketch_object.kind,
8101 ObjectKind::Sketch(Sketch {
8102 args: SketchCtor {
8103 on: Plane::Default(PlaneName::Xy)
8104 },
8105 plane: ObjectId(0),
8106 segments: vec![],
8107 constraints: vec![],
8108 })
8109 );
8110 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8111
8112 let line_ctor = LineCtor {
8113 start: Point2d {
8114 x: Expr::Number(Number {
8115 value: 0.0,
8116 units: NumericSuffix::Mm,
8117 }),
8118 y: Expr::Number(Number {
8119 value: 0.0,
8120 units: NumericSuffix::Mm,
8121 }),
8122 },
8123 end: Point2d {
8124 x: Expr::Number(Number {
8125 value: 10.0,
8126 units: NumericSuffix::Mm,
8127 }),
8128 y: Expr::Number(Number {
8129 value: 10.0,
8130 units: NumericSuffix::Mm,
8131 }),
8132 },
8133 construction: None,
8134 };
8135 let segment = SegmentCtor::Line(line_ctor);
8136 let (src_delta, scene_delta) = frontend
8137 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8138 .await
8139 .unwrap();
8140 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8141 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8142 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8143 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8144 assert_eq!(scene_object.id.0, i);
8145 }
8146
8147 let line = *scene_delta.new_objects.last().unwrap();
8149
8150 let line_ctor = LineCtor {
8151 start: Point2d {
8152 x: Expr::Number(Number {
8153 value: 1.0,
8154 units: NumericSuffix::Mm,
8155 }),
8156 y: Expr::Number(Number {
8157 value: 2.0,
8158 units: NumericSuffix::Mm,
8159 }),
8160 },
8161 end: Point2d {
8162 x: Expr::Number(Number {
8163 value: 13.0,
8164 units: NumericSuffix::Mm,
8165 }),
8166 y: Expr::Number(Number {
8167 value: 14.0,
8168 units: NumericSuffix::Mm,
8169 }),
8170 },
8171 construction: None,
8172 };
8173 let segments = vec![ExistingSegmentCtor {
8174 id: line,
8175 ctor: SegmentCtor::Line(line_ctor),
8176 }];
8177 let (src_delta, scene_delta) = frontend
8178 .edit_segments(&mock_ctx, version, sketch_id, segments)
8179 .await
8180 .unwrap();
8181 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8182 assert_eq!(scene_delta.new_objects, vec![]);
8183 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8184
8185 ctx.close().await;
8186 mock_ctx.close().await;
8187 }
8188
8189 #[tokio::test(flavor = "multi_thread")]
8190 async fn test_new_sketch_add_arc_edit_arc() {
8191 let program = Program::empty();
8192
8193 let mut frontend = FrontendState::new();
8194 frontend.program = program;
8195
8196 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8197 let mock_ctx = ExecutorContext::new_mock(None).await;
8198 let version = Version(0);
8199
8200 let sketch_args = SketchCtor {
8201 on: Plane::Default(PlaneName::Xy),
8202 };
8203 let (_src_delta, scene_delta, sketch_id) = frontend
8204 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8205 .await
8206 .unwrap();
8207 assert_eq!(sketch_id, ObjectId(1));
8208 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8209 let sketch_object = &scene_delta.new_graph.objects[1];
8210 assert_eq!(sketch_object.id, ObjectId(1));
8211 assert_eq!(
8212 sketch_object.kind,
8213 ObjectKind::Sketch(Sketch {
8214 args: SketchCtor {
8215 on: Plane::Default(PlaneName::Xy),
8216 },
8217 plane: ObjectId(0),
8218 segments: vec![],
8219 constraints: vec![],
8220 })
8221 );
8222 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8223
8224 let arc_ctor = ArcCtor {
8225 start: Point2d {
8226 x: Expr::Var(Number {
8227 value: 0.0,
8228 units: NumericSuffix::Mm,
8229 }),
8230 y: Expr::Var(Number {
8231 value: 0.0,
8232 units: NumericSuffix::Mm,
8233 }),
8234 },
8235 end: Point2d {
8236 x: Expr::Var(Number {
8237 value: 10.0,
8238 units: NumericSuffix::Mm,
8239 }),
8240 y: Expr::Var(Number {
8241 value: 10.0,
8242 units: NumericSuffix::Mm,
8243 }),
8244 },
8245 center: Point2d {
8246 x: Expr::Var(Number {
8247 value: 10.0,
8248 units: NumericSuffix::Mm,
8249 }),
8250 y: Expr::Var(Number {
8251 value: 0.0,
8252 units: NumericSuffix::Mm,
8253 }),
8254 },
8255 direction: None,
8256 construction: None,
8257 };
8258 let segment = SegmentCtor::Arc(arc_ctor);
8259 let (src_delta, scene_delta) = frontend
8260 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8261 .await
8262 .unwrap();
8263 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8264 assert_eq!(
8265 scene_delta.new_objects,
8266 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8267 );
8268 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8269 assert_eq!(scene_object.id.0, i);
8270 }
8271 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8272
8273 let arc = *scene_delta.new_objects.last().unwrap();
8275
8276 let arc_ctor = ArcCtor {
8277 start: Point2d {
8278 x: Expr::Var(Number {
8279 value: 1.0,
8280 units: NumericSuffix::Mm,
8281 }),
8282 y: Expr::Var(Number {
8283 value: 2.0,
8284 units: NumericSuffix::Mm,
8285 }),
8286 },
8287 end: Point2d {
8288 x: Expr::Var(Number {
8289 value: 13.0,
8290 units: NumericSuffix::Mm,
8291 }),
8292 y: Expr::Var(Number {
8293 value: 14.0,
8294 units: NumericSuffix::Mm,
8295 }),
8296 },
8297 center: Point2d {
8298 x: Expr::Var(Number {
8299 value: 13.0,
8300 units: NumericSuffix::Mm,
8301 }),
8302 y: Expr::Var(Number {
8303 value: 2.0,
8304 units: NumericSuffix::Mm,
8305 }),
8306 },
8307 direction: None,
8308 construction: None,
8309 };
8310 let segments = vec![ExistingSegmentCtor {
8311 id: arc,
8312 ctor: SegmentCtor::Arc(arc_ctor),
8313 }];
8314 let (src_delta, scene_delta) = frontend
8315 .edit_segments(&mock_ctx, version, sketch_id, segments)
8316 .await
8317 .unwrap();
8318 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8319 assert_eq!(scene_delta.new_objects, vec![]);
8320 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8321
8322 ctx.close().await;
8323 mock_ctx.close().await;
8324 }
8325
8326 #[tokio::test(flavor = "multi_thread")]
8327 async fn test_new_sketch_add_circle_edit_circle() {
8328 let program = Program::empty();
8329
8330 let mut frontend = FrontendState::new();
8331 frontend.program = program;
8332
8333 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8334 let mock_ctx = ExecutorContext::new_mock(None).await;
8335 let version = Version(0);
8336
8337 let sketch_args = SketchCtor {
8338 on: Plane::Default(PlaneName::Xy),
8339 };
8340 let (_src_delta, _scene_delta, sketch_id) = frontend
8341 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8342 .await
8343 .unwrap();
8344
8345 let circle_ctor = CircleCtor {
8347 start: Point2d {
8348 x: Expr::Var(Number {
8349 value: 5.0,
8350 units: NumericSuffix::Mm,
8351 }),
8352 y: Expr::Var(Number {
8353 value: 0.0,
8354 units: NumericSuffix::Mm,
8355 }),
8356 },
8357 center: Point2d {
8358 x: Expr::Var(Number {
8359 value: 0.0,
8360 units: NumericSuffix::Mm,
8361 }),
8362 y: Expr::Var(Number {
8363 value: 0.0,
8364 units: NumericSuffix::Mm,
8365 }),
8366 },
8367 construction: None,
8368 };
8369 let segment = SegmentCtor::Circle(circle_ctor);
8370 let (src_delta, scene_delta) = frontend
8371 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8372 .await
8373 .unwrap();
8374 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8375 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8377 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8378
8379 let circle = *scene_delta.new_objects.last().unwrap();
8380
8381 let circle_ctor = CircleCtor {
8383 start: Point2d {
8384 x: Expr::Var(Number {
8385 value: 10.0,
8386 units: NumericSuffix::Mm,
8387 }),
8388 y: Expr::Var(Number {
8389 value: 0.0,
8390 units: NumericSuffix::Mm,
8391 }),
8392 },
8393 center: Point2d {
8394 x: Expr::Var(Number {
8395 value: 3.0,
8396 units: NumericSuffix::Mm,
8397 }),
8398 y: Expr::Var(Number {
8399 value: 4.0,
8400 units: NumericSuffix::Mm,
8401 }),
8402 },
8403 construction: None,
8404 };
8405 let segments = vec![ExistingSegmentCtor {
8406 id: circle,
8407 ctor: SegmentCtor::Circle(circle_ctor),
8408 }];
8409 let (src_delta, scene_delta) = frontend
8410 .edit_segments(&mock_ctx, version, sketch_id, segments)
8411 .await
8412 .unwrap();
8413 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8414 assert_eq!(scene_delta.new_objects, vec![]);
8415 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8416
8417 ctx.close().await;
8418 mock_ctx.close().await;
8419 }
8420
8421 #[tokio::test(flavor = "multi_thread")]
8422 async fn test_delete_circle() {
8423 let initial_source = "sketch001 = sketch(on = XY) {
8424 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8425}
8426";
8427
8428 let program = Program::parse(initial_source).unwrap().0.unwrap();
8429 let mut frontend = FrontendState::new();
8430
8431 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8432 let mock_ctx = ExecutorContext::new_mock(None).await;
8433 let version = Version(0);
8434
8435 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8436 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8437 let sketch_id = sketch_object.id;
8438 let sketch = expect_sketch(sketch_object);
8439
8440 assert_eq!(sketch.segments.len(), 3);
8442 let circle_id = sketch.segments[2];
8443
8444 let (src_delta, scene_delta) = frontend
8446 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8447 .await
8448 .unwrap();
8449 insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8450 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8451 let new_sketch = expect_sketch(new_sketch_object);
8452 assert_eq!(new_sketch.segments.len(), 0);
8453
8454 ctx.close().await;
8455 mock_ctx.close().await;
8456 }
8457
8458 #[tokio::test(flavor = "multi_thread")]
8459 async fn test_edit_circle_via_point() {
8460 let initial_source = "sketch001 = sketch(on = XY) {
8461 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8462}
8463";
8464
8465 let program = Program::parse(initial_source).unwrap().0.unwrap();
8466 let mut frontend = FrontendState::new();
8467
8468 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8469 let mock_ctx = ExecutorContext::new_mock(None).await;
8470 let version = Version(0);
8471
8472 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8473 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8474 let sketch_id = sketch_object.id;
8475 let sketch = expect_sketch(sketch_object);
8476
8477 let circle_id = sketch
8479 .segments
8480 .iter()
8481 .copied()
8482 .find(|seg_id| {
8483 matches!(
8484 &frontend.scene_graph.objects[seg_id.0].kind,
8485 ObjectKind::Segment {
8486 segment: Segment::Circle(_)
8487 }
8488 )
8489 })
8490 .expect("Expected a circle segment in sketch");
8491 let circle_object = &frontend.scene_graph.objects[circle_id.0];
8492 let ObjectKind::Segment {
8493 segment: Segment::Circle(circle),
8494 } = &circle_object.kind
8495 else {
8496 panic!("Expected circle segment, got: {:?}", circle_object.kind);
8497 };
8498 let start_point_id = circle.start;
8499
8500 let segments = vec![ExistingSegmentCtor {
8502 id: start_point_id,
8503 ctor: SegmentCtor::Point(PointCtor {
8504 position: Point2d {
8505 x: Expr::Var(Number {
8506 value: 7.0,
8507 units: NumericSuffix::Mm,
8508 }),
8509 y: Expr::Var(Number {
8510 value: 1.0,
8511 units: NumericSuffix::Mm,
8512 }),
8513 },
8514 }),
8515 }];
8516 let (src_delta, _scene_delta) = frontend
8517 .edit_segments(&mock_ctx, version, sketch_id, segments)
8518 .await
8519 .unwrap();
8520 insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8521
8522 ctx.close().await;
8523 mock_ctx.close().await;
8524 }
8525
8526 #[tokio::test(flavor = "multi_thread")]
8527 async fn test_add_line_when_sketch_block_uses_variable() {
8528 let initial_source = "s = sketch(on = XY) {}
8529";
8530
8531 let program = Program::parse(initial_source).unwrap().0.unwrap();
8532
8533 let mut frontend = FrontendState::new();
8534
8535 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8536 let mock_ctx = ExecutorContext::new_mock(None).await;
8537 let version = Version(0);
8538
8539 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8540 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8541 let sketch_id = sketch_object.id;
8542
8543 let line_ctor = LineCtor {
8544 start: Point2d {
8545 x: Expr::Number(Number {
8546 value: 0.0,
8547 units: NumericSuffix::Mm,
8548 }),
8549 y: Expr::Number(Number {
8550 value: 0.0,
8551 units: NumericSuffix::Mm,
8552 }),
8553 },
8554 end: Point2d {
8555 x: Expr::Number(Number {
8556 value: 10.0,
8557 units: NumericSuffix::Mm,
8558 }),
8559 y: Expr::Number(Number {
8560 value: 10.0,
8561 units: NumericSuffix::Mm,
8562 }),
8563 },
8564 construction: None,
8565 };
8566 let segment = SegmentCtor::Line(line_ctor);
8567 let (src_delta, scene_delta) = frontend
8568 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8569 .await
8570 .unwrap();
8571 insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8572 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8573 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8574
8575 ctx.close().await;
8576 mock_ctx.close().await;
8577 }
8578
8579 #[tokio::test(flavor = "multi_thread")]
8580 async fn test_new_sketch_add_line_delete_sketch() {
8581 let program = Program::empty();
8582
8583 let mut frontend = FrontendState::new();
8584 frontend.program = program;
8585
8586 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8587 let mock_ctx = ExecutorContext::new_mock(None).await;
8588 let version = Version(0);
8589
8590 let sketch_args = SketchCtor {
8591 on: Plane::Default(PlaneName::Xy),
8592 };
8593 let (_src_delta, scene_delta, sketch_id) = frontend
8594 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8595 .await
8596 .unwrap();
8597 assert_eq!(sketch_id, ObjectId(1));
8598 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8599 let sketch_object = &scene_delta.new_graph.objects[1];
8600 assert_eq!(sketch_object.id, ObjectId(1));
8601 assert_eq!(
8602 sketch_object.kind,
8603 ObjectKind::Sketch(Sketch {
8604 args: SketchCtor {
8605 on: Plane::Default(PlaneName::Xy)
8606 },
8607 plane: ObjectId(0),
8608 segments: vec![],
8609 constraints: vec![],
8610 })
8611 );
8612 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8613
8614 let line_ctor = LineCtor {
8615 start: Point2d {
8616 x: Expr::Number(Number {
8617 value: 0.0,
8618 units: NumericSuffix::Mm,
8619 }),
8620 y: Expr::Number(Number {
8621 value: 0.0,
8622 units: NumericSuffix::Mm,
8623 }),
8624 },
8625 end: Point2d {
8626 x: Expr::Number(Number {
8627 value: 10.0,
8628 units: NumericSuffix::Mm,
8629 }),
8630 y: Expr::Number(Number {
8631 value: 10.0,
8632 units: NumericSuffix::Mm,
8633 }),
8634 },
8635 construction: None,
8636 };
8637 let segment = SegmentCtor::Line(line_ctor);
8638 let (src_delta, scene_delta) = frontend
8639 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8640 .await
8641 .unwrap();
8642 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8643 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8644
8645 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8646 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8647 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8648
8649 ctx.close().await;
8650 mock_ctx.close().await;
8651 }
8652
8653 #[tokio::test(flavor = "multi_thread")]
8654 async fn test_delete_sketch_when_sketch_block_uses_variable() {
8655 let initial_source = "s = sketch(on = XY) {}
8656";
8657
8658 let program = Program::parse(initial_source).unwrap().0.unwrap();
8659
8660 let mut frontend = FrontendState::new();
8661
8662 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8663 let version = Version(0);
8664
8665 frontend.hack_set_program(&ctx, program).await.unwrap();
8666 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8667 let sketch_id = sketch_object.id;
8668
8669 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8670 insta::assert_snapshot!(
8671 "test_delete_sketch_when_sketch_block_uses_variable",
8672 src_delta.text.as_str()
8673 );
8674 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8675
8676 ctx.close().await;
8677 }
8678
8679 #[tokio::test(flavor = "multi_thread")]
8680 async fn test_delete_sketch_after_comment() {
8681 let initial_source = "sketch001 = sketch(on = XZ) {
8682}
8683";
8684
8685 let program = Program::parse(initial_source).unwrap().0.unwrap();
8686 let mut frontend = FrontendState::new();
8687
8688 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8689 let version = Version(0);
8690
8691 frontend.hack_set_program(&ctx, program).await.unwrap();
8692 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8693 let sketch_id = sketch_object.id;
8694 let original_source = sketch_object.source.clone();
8695
8696 let commented_source = "// test 1
8697sketch001 = sketch(on = XZ) {
8698}
8699";
8700 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8701 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8702
8703 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8704 assert_eq!(cached_sketch_object.source, original_source);
8705
8706 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8707 assert!(
8708 !src_delta.text.contains("sketch001"),
8709 "sketch was not deleted: {}",
8710 src_delta.text
8711 );
8712 insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8714 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8715
8716 ctx.close().await;
8717 }
8718
8719 #[tokio::test(flavor = "multi_thread")]
8720 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8721 let initial_source = "sketch001 = sketch(on = XZ) {
8722}
8723foo = 1
8724";
8725
8726 let program = Program::parse(initial_source).unwrap().0.unwrap();
8727 let mut frontend = FrontendState::new();
8728
8729 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8730 let version = Version(0);
8731
8732 frontend.hack_set_program(&ctx, program).await.unwrap();
8733 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8734 let sketch_id = sketch_object.id;
8735
8736 let commented_source = "// keep me
8737sketch001 = sketch(on = XZ) {
8738}
8739foo = 1
8740";
8741 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8742 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8743
8744 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8745 insta::assert_snapshot!(
8747 "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8748 src_delta.text.as_str()
8749 );
8750
8751 ctx.close().await;
8752 }
8753
8754 #[tokio::test(flavor = "multi_thread")]
8755 async fn test_delete_segment_preserves_pre_comment() {
8756 let initial_source = "\
8757sketch(on = XY) {
8758 point(at = [var 1, var 2])
8759 // describe the middle point
8760 point(at = [var 3, var 4])
8761 point(at = [var 5, var 6])
8762}
8763";
8764
8765 let program = Program::parse(initial_source).unwrap().0.unwrap();
8766 let mut frontend = FrontendState::new();
8767
8768 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8769 let mock_ctx = ExecutorContext::new_mock(None).await;
8770 let version = Version(0);
8771
8772 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8773 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8774 let sketch_id = sketch_object.id;
8775 let sketch = expect_sketch(sketch_object);
8776
8777 let middle_point_id = *sketch.segments.get(1).unwrap();
8778
8779 let (src_delta, _scene_delta) = frontend
8780 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8781 .await
8782 .unwrap();
8783 insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8786
8787 ctx.close().await;
8788 mock_ctx.close().await;
8789 }
8790
8791 #[tokio::test(flavor = "multi_thread")]
8792 async fn test_delete_last_segment_preserves_pre_comment() {
8793 let initial_source = "\
8794sketch(on = XY) {
8795 point(at = [var 1, var 2])
8796 // describe the trailing point
8797 point(at = [var 3, var 4])
8798}
8799";
8800
8801 let program = Program::parse(initial_source).unwrap().0.unwrap();
8802 let mut frontend = FrontendState::new();
8803
8804 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8805 let mock_ctx = ExecutorContext::new_mock(None).await;
8806 let version = Version(0);
8807
8808 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8809 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8810 let sketch_id = sketch_object.id;
8811 let sketch = expect_sketch(sketch_object);
8812
8813 let last_point_id = *sketch.segments.last().unwrap();
8814
8815 let (src_delta, _scene_delta) = frontend
8816 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8817 .await
8818 .unwrap();
8819 insta::assert_snapshot!(
8822 "test_delete_last_segment_preserves_pre_comment",
8823 src_delta.text.as_str()
8824 );
8825
8826 ctx.close().await;
8827 mock_ctx.close().await;
8828 }
8829
8830 #[tokio::test(flavor = "multi_thread")]
8831 async fn test_delete_segment_drops_inline_trailing_comment() {
8832 let initial_source = "\
8833sketch(on = XY) {
8834 point(at = [var 1, var 2])
8835 point(at = [var 3, var 4]) // same-line note that gets dropped
8836 point(at = [var 5, var 6])
8837}
8838";
8839
8840 let program = Program::parse(initial_source).unwrap().0.unwrap();
8841 let mut frontend = FrontendState::new();
8842
8843 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8844 let mock_ctx = ExecutorContext::new_mock(None).await;
8845 let version = Version(0);
8846
8847 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8848 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8849 let sketch_id = sketch_object.id;
8850 let sketch = expect_sketch(sketch_object);
8851
8852 let middle_point_id = *sketch.segments.get(1).unwrap();
8853
8854 let (src_delta, _scene_delta) = frontend
8855 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8856 .await
8857 .unwrap();
8858 assert!(
8860 !src_delta.text.contains("same-line note"),
8861 "inline comment should have been removed: {}",
8862 src_delta.text
8863 );
8864
8865 ctx.close().await;
8866 mock_ctx.close().await;
8867 }
8868
8869 #[tokio::test(flavor = "multi_thread")]
8870 async fn test_delete_segments_preserves_block_comments_across_positions() {
8871 let initial_source = "\
8879sketch(on = XY) {
8880 /* above first - moves to middle */
8881 point(at = [var 1, var 2]) /* same-line on first - dropped */
8882 /* above middle - stays */
8883 point(at = [var 3, var 4])
8884 /* above last - moves to trailing meta */
8885 point(at = [var 5, var 6])
8886}
8887";
8888
8889 let program = Program::parse(initial_source).unwrap().0.unwrap();
8890 let mut frontend = FrontendState::new();
8891
8892 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8893 let mock_ctx = ExecutorContext::new_mock(None).await;
8894 let version = Version(0);
8895
8896 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8897 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8898 let sketch_id = sketch_object.id;
8899 let sketch = expect_sketch(sketch_object);
8900
8901 let first_point_id = *sketch.segments.first().unwrap();
8902 let last_point_id = *sketch.segments.last().unwrap();
8903
8904 let (src_delta, _scene_delta) = frontend
8905 .delete_objects(
8906 &mock_ctx,
8907 version,
8908 sketch_id,
8909 Vec::new(),
8910 vec![first_point_id, last_point_id],
8911 )
8912 .await
8913 .unwrap();
8914 insta::assert_snapshot!(
8915 "test_delete_segments_preserves_block_comments_across_positions",
8916 src_delta.text.as_str()
8917 );
8918
8919 ctx.close().await;
8920 mock_ctx.close().await;
8921 }
8922
8923 #[tokio::test(flavor = "multi_thread")]
8924 async fn test_edit_line_when_editing_its_start_point() {
8925 let initial_source = "\
8926sketch(on = XY) {
8927 line(start = [var 1, var 2], end = [var 3, var 4])
8928}
8929";
8930
8931 let program = Program::parse(initial_source).unwrap().0.unwrap();
8932
8933 let mut frontend = FrontendState::new();
8934
8935 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8936 let mock_ctx = ExecutorContext::new_mock(None).await;
8937 let version = Version(0);
8938
8939 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8940 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8941 let sketch_id = sketch_object.id;
8942 let sketch = expect_sketch(sketch_object);
8943
8944 let point_id = *sketch.segments.first().unwrap();
8945
8946 let point_ctor = PointCtor {
8947 position: Point2d {
8948 x: Expr::Var(Number {
8949 value: 5.0,
8950 units: NumericSuffix::Inch,
8951 }),
8952 y: Expr::Var(Number {
8953 value: 6.0,
8954 units: NumericSuffix::Inch,
8955 }),
8956 },
8957 };
8958 let segments = vec![ExistingSegmentCtor {
8959 id: point_id,
8960 ctor: SegmentCtor::Point(point_ctor),
8961 }];
8962 let (src_delta, scene_delta) = frontend
8963 .edit_segments(&mock_ctx, version, sketch_id, segments)
8964 .await
8965 .unwrap();
8966 insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
8967 assert_eq!(scene_delta.new_objects, vec![]);
8968 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8969
8970 ctx.close().await;
8971 mock_ctx.close().await;
8972 }
8973
8974 #[tokio::test(flavor = "multi_thread")]
8975 async fn test_edit_line_when_editing_its_end_point() {
8976 let initial_source = "\
8977sketch(on = XY) {
8978 line(start = [var 1, var 2], end = [var 3, var 4])
8979}
8980";
8981
8982 let program = Program::parse(initial_source).unwrap().0.unwrap();
8983
8984 let mut frontend = FrontendState::new();
8985
8986 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8987 let mock_ctx = ExecutorContext::new_mock(None).await;
8988 let version = Version(0);
8989
8990 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8991 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8992 let sketch_id = sketch_object.id;
8993 let sketch = expect_sketch(sketch_object);
8994 let point_id = *sketch.segments.get(1).unwrap();
8995
8996 let point_ctor = PointCtor {
8997 position: Point2d {
8998 x: Expr::Var(Number {
8999 value: 5.0,
9000 units: NumericSuffix::Inch,
9001 }),
9002 y: Expr::Var(Number {
9003 value: 6.0,
9004 units: NumericSuffix::Inch,
9005 }),
9006 },
9007 };
9008 let segments = vec![ExistingSegmentCtor {
9009 id: point_id,
9010 ctor: SegmentCtor::Point(point_ctor),
9011 }];
9012 let (src_delta, scene_delta) = frontend
9013 .edit_segments(&mock_ctx, version, sketch_id, segments)
9014 .await
9015 .unwrap();
9016 insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9017 assert_eq!(scene_delta.new_objects, vec![]);
9018 assert_eq!(
9019 scene_delta.new_graph.objects.len(),
9020 5,
9021 "{:#?}",
9022 scene_delta.new_graph.objects
9023 );
9024
9025 ctx.close().await;
9026 mock_ctx.close().await;
9027 }
9028
9029 #[tokio::test(flavor = "multi_thread")]
9030 async fn test_edit_line_with_coincident_feedback() {
9031 let initial_source = "\
9032sketch(on = XY) {
9033 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9034 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9035 fixed([line1.start, [0, 0]])
9036 coincident([line1.end, line2.start])
9037 equalLength([line1, line2])
9038}
9039";
9040
9041 let program = Program::parse(initial_source).unwrap().0.unwrap();
9042
9043 let mut frontend = FrontendState::new();
9044
9045 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9046 let mock_ctx = ExecutorContext::new_mock(None).await;
9047 let version = Version(0);
9048
9049 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9050 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9051 let sketch_id = sketch_object.id;
9052 let sketch = expect_sketch(sketch_object);
9053 let line2_end_id = *sketch.segments.get(4).unwrap();
9054
9055 let segments = vec![ExistingSegmentCtor {
9056 id: line2_end_id,
9057 ctor: SegmentCtor::Point(PointCtor {
9058 position: Point2d {
9059 x: Expr::Var(Number {
9060 value: 9.0,
9061 units: NumericSuffix::None,
9062 }),
9063 y: Expr::Var(Number {
9064 value: 10.0,
9065 units: NumericSuffix::None,
9066 }),
9067 },
9068 }),
9069 }];
9070 let (src_delta, scene_delta) = frontend
9071 .edit_segments(&mock_ctx, version, sketch_id, segments)
9072 .await
9073 .unwrap();
9074 insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9075 assert_eq!(
9076 scene_delta.new_graph.objects.len(),
9077 11,
9078 "{:#?}",
9079 scene_delta.new_graph.objects
9080 );
9081
9082 ctx.close().await;
9083 mock_ctx.close().await;
9084 }
9085
9086 #[tokio::test(flavor = "multi_thread")]
9087 async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9088 let initial_source = "\
9089sketch(on = XY) {
9090 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9091 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9092 fixed([line1.start, [0, 0]])
9093 coincident([line1.end, line2.start])
9094 equalLength([line1, line2])
9095}
9096";
9097
9098 let program = Program::parse(initial_source).unwrap().0.unwrap();
9099 let mut frontend = FrontendState::new();
9100 let mock_ctx = ExecutorContext::new_mock(None).await;
9101 let version = Version(0);
9102
9103 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9104 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9105 let sketch_id = sketch_object.id;
9106 let sketch = expect_sketch(sketch_object);
9107 let line2_end_id = *sketch.segments.get(4).unwrap();
9108
9109 let segments = vec![ExistingSegmentCtor {
9110 id: line2_end_id,
9111 ctor: SegmentCtor::Point(PointCtor {
9112 position: Point2d {
9113 x: Expr::Var(Number {
9114 value: 9.0,
9115 units: NumericSuffix::None,
9116 }),
9117 y: Expr::Var(Number {
9118 value: 10.0,
9119 units: NumericSuffix::None,
9120 }),
9121 },
9122 }),
9123 }];
9124 let (edited_source, _) = frontend
9125 .edit_segments(&mock_ctx, version, sketch_id, segments)
9126 .await
9127 .unwrap();
9128
9129 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9130 assert_eq!(mock_source.text, edited_source.text);
9131
9132 mock_ctx.close().await;
9133 }
9134
9135 #[tokio::test(flavor = "multi_thread")]
9138 async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9139 let initial_source = "\
9140sketch(on = XY) {
9141 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9142 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9143 fixed([line1.start, [0, 0]])
9144 coincident([line1.end, line2.start])
9145 equalLength([line1, line2])
9146}
9147";
9148
9149 let program = Program::parse(initial_source).unwrap().0.unwrap();
9150 let mut frontend = FrontendState::new();
9151 let mock_ctx = ExecutorContext::new_mock(None).await;
9152 let version = Version(0);
9153
9154 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9155 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9156 let sketch_id = sketch_object.id;
9157 let sketch = expect_sketch(sketch_object);
9158 let line2_end_id = *sketch.segments.get(4).unwrap();
9159
9160 let segments = vec![ExistingSegmentCtor {
9161 id: line2_end_id,
9162 ctor: SegmentCtor::Point(PointCtor {
9163 position: Point2d {
9164 x: Expr::Var(Number {
9165 value: 9.0,
9166 units: NumericSuffix::None,
9167 }),
9168 y: Expr::Var(Number {
9169 value: 10.0,
9170 units: NumericSuffix::None,
9171 }),
9172 },
9173 }),
9174 }];
9175 let (preview_source, preview_delta) = frontend
9176 .edit_segments_with_options(
9177 &mock_ctx,
9178 version,
9179 sketch_id,
9180 segments,
9181 EditSegmentsOptions {
9182 anchor_segment_ids: Some(vec![line2_end_id]),
9183 drag_anchors: Vec::new(),
9184 constraint_label_edits: Vec::new(),
9185 commit_solved_initial_guesses: false,
9186 },
9187 )
9188 .await
9189 .unwrap();
9190
9191 assert!(
9192 !preview_delta.exec_outcome.var_solutions.is_empty(),
9193 "preview solve should still solve and return geometry feedback"
9194 );
9195 assert!(
9196 preview_source
9197 .text
9198 .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9199 );
9200 assert!(
9201 preview_source
9202 .text
9203 .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9204 );
9205
9206 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9207 assert_eq!(mock_source.text, preview_source.text);
9208
9209 mock_ctx.close().await;
9210 }
9211
9212 #[tokio::test(flavor = "multi_thread")]
9213 async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9214 let initial_source = "\
9215sketch(on = XY) {
9216 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9217}
9218";
9219
9220 let program = Program::parse(initial_source).unwrap().0.unwrap();
9221 let mut frontend = FrontendState::new();
9222 let mock_ctx = ExecutorContext::new_mock(None).await;
9223 let version = Version(0);
9224
9225 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9226 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9227 let sketch_id = sketch_object.id;
9228 let sketch = expect_sketch(sketch_object);
9229 let line_end_id = *sketch.segments.get(1).unwrap();
9230
9231 let constraint = Constraint::Fixed(Fixed {
9232 points: vec![FixedPoint {
9233 point: line_end_id,
9234 position: Point2d {
9235 x: Number {
9236 value: 20.0,
9237 units: NumericSuffix::Mm,
9238 },
9239 y: Number {
9240 value: 0.0,
9241 units: NumericSuffix::Mm,
9242 },
9243 },
9244 }],
9245 });
9246 let (constraint_source, _) = frontend
9247 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9248 .await
9249 .unwrap();
9250
9251 assert!(
9252 constraint_source
9253 .text
9254 .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9255 "{}",
9256 constraint_source.text
9257 );
9258 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9259 assert_eq!(mock_source.text, constraint_source.text);
9260
9261 mock_ctx.close().await;
9262 }
9263
9264 #[test]
9265 fn test_no_solver_feedback_preserves_original_source() {
9266 let initial_source = "\
9267@settings(defaultLengthUnit = in, kclVersion = 2.0)
9268cylinder = startSketchOn(XY)
9269 |> circle(center= [0, 0], radius= 22)
9270 |> extrude(length = 14)
9271";
9272 let mut frontend = FrontendState::new();
9273 frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9274 let outcome = ExecOutcome {
9275 variables: Default::default(),
9276 test_program_memory: Default::default(),
9277 operations: Default::default(),
9278 artifact_graph: Default::default(),
9279 scene_objects: Default::default(),
9280 source_range_to_object: Default::default(),
9281 var_solutions: Default::default(),
9282 refactor_metadata: Default::default(),
9283 issues: Default::default(),
9284 filenames: Default::default(),
9285 source_files: Default::default(),
9286 default_planes: Default::default(),
9287 };
9288
9289 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9290
9291 assert_eq!(source_delta.text, initial_source);
9292 }
9293
9294 #[tokio::test(flavor = "multi_thread")]
9297 async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9298 let initial_source = "\
9299sketch(on = XY) {
9300 point1 = point(at = [var 0mm, var 0mm])
9301 point2 = point(at = [var 0mm, var 0mm])
9302 coincident([point1, point2])
9303}
9304";
9305
9306 let program = Program::parse(initial_source).unwrap().0.unwrap();
9307 let mut frontend = FrontendState::new();
9308 let mock_ctx = ExecutorContext::new_mock(None).await;
9309 let version = Version(0);
9310
9311 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9312 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9313 let sketch_id = sketch_object.id;
9314 let sketch = expect_sketch(sketch_object);
9315 let point1_id = sketch.segments[0];
9316 let point2_id = sketch.segments[1];
9317
9318 let segments = vec![
9319 ExistingSegmentCtor {
9320 id: point1_id,
9321 ctor: SegmentCtor::Point(PointCtor {
9322 position: point_expr_mm(10.0, 0.0),
9323 }),
9324 },
9325 ExistingSegmentCtor {
9326 id: point2_id,
9327 ctor: SegmentCtor::Point(PointCtor {
9328 position: point_expr_mm(100.0, 0.0),
9329 }),
9330 },
9331 ];
9332 let (_, scene_delta) = frontend
9333 .edit_segments_with_options(
9334 &mock_ctx,
9335 version,
9336 sketch_id,
9337 segments,
9338 EditSegmentsOptions {
9339 anchor_segment_ids: Some(vec![point1_id]),
9340 drag_anchors: Vec::new(),
9341 constraint_label_edits: Vec::new(),
9342 commit_solved_initial_guesses: true,
9343 },
9344 )
9345 .await
9346 .unwrap();
9347
9348 assert_point_position_close(
9349 point_position(&scene_delta.new_graph, point1_id),
9350 point_number_mm(10.0, 0.0),
9351 );
9352 assert_point_position_close(
9353 point_position(&scene_delta.new_graph, point2_id),
9354 point_number_mm(10.0, 0.0),
9355 );
9356
9357 mock_ctx.close().await;
9358 }
9359
9360 fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9363 use std::cell::RefCell;
9364 struct Collector {
9365 target: f64,
9366 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9367 }
9368 impl<'a> crate::walk::Visitor<'a> for &Collector {
9369 type Error = crate::front::Error;
9370 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9371 if let crate::walk::Node::SketchVar(sketch_var) = node
9372 && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9373 && (initial.value - self.target).abs() < 1e-9
9374 {
9375 self.out
9376 .borrow_mut()
9377 .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9378 }
9379 for child in node.children().iter() {
9380 if !child.visit(*self)? {
9381 return Ok(false);
9382 }
9383 }
9384 Ok(true)
9385 }
9386 }
9387 let collector = Collector {
9388 target: value,
9389 out: Default::default(),
9390 };
9391 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9392 collector.out.into_inner()
9393 }
9394
9395 fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9398 use std::cell::RefCell;
9399 struct Collector {
9400 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9401 }
9402 impl<'a> crate::walk::Visitor<'a> for &Collector {
9403 type Error = crate::front::Error;
9404 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9405 if let crate::walk::Node::SketchVar(sketch_var) = node
9406 && let Some(node_path) = &sketch_var.node_path
9407 {
9408 self.out
9409 .borrow_mut()
9410 .push((SourceRange::from(sketch_var), node_path.clone()));
9411 }
9412 for child in node.children().iter() {
9413 if !child.visit(*self)? {
9414 return Ok(false);
9415 }
9416 }
9417 Ok(true)
9418 }
9419 }
9420 let collector = Collector {
9421 out: Default::default(),
9422 };
9423 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9424 collector.out.into_inner()
9425 }
9426
9427 fn empty_exec_outcome_with_var_solutions(
9428 var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9429 ) -> ExecOutcome {
9430 ExecOutcome {
9431 variables: Default::default(),
9432 test_program_memory: Default::default(),
9433 operations: Default::default(),
9434 artifact_graph: Default::default(),
9435 scene_objects: Default::default(),
9436 source_range_to_object: Default::default(),
9437 var_solutions,
9438 refactor_metadata: Default::default(),
9439 issues: Default::default(),
9440 filenames: Default::default(),
9441 source_files: Default::default(),
9442 default_planes: Default::default(),
9443 }
9444 }
9445
9446 #[test]
9450 fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9451 let initial_source = "\
9452sketch(on = XY) {
9453 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9454}
9455";
9456 let program = Program::parse(initial_source).unwrap().0.unwrap();
9457 let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9458 assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9459 let (literal_range, node_path) = matches.into_iter().next().unwrap();
9460
9461 let mut frontend = FrontendState::new();
9462 frontend.program = program;
9463
9464 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9465 literal_range,
9466 Some(node_path),
9467 Number {
9468 value: 25.0,
9469 units: NumericSuffix::Mm,
9470 },
9471 )]);
9472
9473 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9474
9475 insta::assert_snapshot!(
9476 "test_commit_var_solution_by_node_path_updates_sketch_var",
9477 source_delta.text
9478 );
9479 }
9480
9481 #[test]
9488 fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9489 let compact_source = "\
9490sketch(on = XY) {
9491 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9492}
9493";
9494 let padded_source = "\
9495// added comment\n// added comment\n\nsketch(on = XY) {
9496 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9497}
9498";
9499 let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9500 let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9501
9502 let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9503 .into_iter()
9504 .next()
9505 .expect("expected `var 10mm` in compact source");
9506 let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9507 .into_iter()
9508 .next()
9509 .expect("expected `var 10mm` in padded source");
9510
9511 assert_ne!(
9512 compact_match.0, padded_match.0,
9513 "byte offsets must differ for this test to be meaningful"
9514 );
9515 assert_eq!(
9516 compact_match.1, padded_match.1,
9517 "node paths must agree across whitespace; that's the whole point of NodePath",
9518 );
9519
9520 let mut frontend = FrontendState::new();
9521 frontend.program = padded_program;
9522
9523 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9525 compact_match.0,
9526 Some(compact_match.1),
9527 Number {
9528 value: 30.0,
9529 units: NumericSuffix::Mm,
9530 },
9531 )]);
9532
9533 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9534
9535 insta::assert_snapshot!(
9536 "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9537 source_delta.text
9538 );
9539 }
9540
9541 #[test]
9545 fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9546 let initial_source = "\
9547sketch(on = XY) {
9548 line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9549}
9550";
9551 let program = Program::parse(initial_source).unwrap().0.unwrap();
9552
9553 let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9554 .into_iter()
9555 .next()
9556 .expect("expected `var 10mm`");
9557 let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9558 .into_iter()
9559 .next()
9560 .expect("expected `var 20mm`");
9561
9562 let mut frontend = FrontendState::new();
9563 frontend.program = program;
9564
9565 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9567 var_20.0,
9568 Some(var_10.1),
9569 Number {
9570 value: 33.0,
9571 units: NumericSuffix::Mm,
9572 },
9573 )]);
9574
9575 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9576
9577 insta::assert_snapshot!(
9578 "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9579 source_delta.text
9580 );
9581 }
9582
9583 #[test]
9590 fn test_commit_var_solution_writes_back_into_bare_var() {
9591 let initial_source = "\
9592@settings(experimentalFeatures = allow, kclVersion = 2.0)
9593sketch(on = XY) {
9594 line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9595}
9596";
9597 let program = Program::parse(initial_source).unwrap().0.unwrap();
9598
9599 let bare = collect_all_sketch_vars(&program)
9602 .into_iter()
9603 .find(|(range, _)| {
9604 range.end() - range.start() == 3
9606 })
9607 .expect("expected at least one bare `var`");
9608
9609 let mut frontend = FrontendState::new();
9610 frontend.program = program;
9611
9612 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9613 bare.0,
9614 Some(bare.1),
9615 Number {
9616 value: 7.0,
9617 units: NumericSuffix::Mm,
9618 },
9619 )]);
9620
9621 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9622
9623 insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9627 }
9628
9629 #[tokio::test(flavor = "multi_thread")]
9630 async fn test_delete_point_without_var() {
9631 let initial_source = "\
9632sketch(on = XY) {
9633 point(at = [var 1, var 2])
9634 point(at = [var 3, var 4])
9635 point(at = [var 5, var 6])
9636}
9637";
9638
9639 let program = Program::parse(initial_source).unwrap().0.unwrap();
9640
9641 let mut frontend = FrontendState::new();
9642
9643 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9644 let mock_ctx = ExecutorContext::new_mock(None).await;
9645 let version = Version(0);
9646
9647 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9648 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9649 let sketch_id = sketch_object.id;
9650 let sketch = expect_sketch(sketch_object);
9651
9652 let point_id = *sketch.segments.get(1).unwrap();
9653
9654 let (src_delta, scene_delta) = frontend
9655 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9656 .await
9657 .unwrap();
9658 insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9659 assert_eq!(scene_delta.new_objects, vec![]);
9660 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9661
9662 ctx.close().await;
9663 mock_ctx.close().await;
9664 }
9665
9666 #[tokio::test(flavor = "multi_thread")]
9667 async fn test_delete_point_with_var() {
9668 let initial_source = "\
9669sketch(on = XY) {
9670 point(at = [var 1, var 2])
9671 point1 = point(at = [var 3, var 4])
9672 point(at = [var 5, var 6])
9673}
9674";
9675
9676 let program = Program::parse(initial_source).unwrap().0.unwrap();
9677
9678 let mut frontend = FrontendState::new();
9679
9680 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9681 let mock_ctx = ExecutorContext::new_mock(None).await;
9682 let version = Version(0);
9683
9684 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9685 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9686 let sketch_id = sketch_object.id;
9687 let sketch = expect_sketch(sketch_object);
9688
9689 let point_id = *sketch.segments.get(1).unwrap();
9690
9691 let (src_delta, scene_delta) = frontend
9692 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9693 .await
9694 .unwrap();
9695 insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9696 assert_eq!(scene_delta.new_objects, vec![]);
9697 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9698
9699 ctx.close().await;
9700 mock_ctx.close().await;
9701 }
9702
9703 #[tokio::test(flavor = "multi_thread")]
9704 async fn test_delete_multiple_points() {
9705 let initial_source = "\
9706sketch(on = XY) {
9707 point(at = [var 1, var 2])
9708 point1 = point(at = [var 3, var 4])
9709 point(at = [var 5, var 6])
9710}
9711";
9712
9713 let program = Program::parse(initial_source).unwrap().0.unwrap();
9714
9715 let mut frontend = FrontendState::new();
9716
9717 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9718 let mock_ctx = ExecutorContext::new_mock(None).await;
9719 let version = Version(0);
9720
9721 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9722 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9723 let sketch_id = sketch_object.id;
9724
9725 let sketch = expect_sketch(sketch_object);
9726
9727 let point1_id = *sketch.segments.first().unwrap();
9728 let point2_id = *sketch.segments.get(1).unwrap();
9729
9730 let (src_delta, scene_delta) = frontend
9731 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9732 .await
9733 .unwrap();
9734 insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9735 assert_eq!(scene_delta.new_objects, vec![]);
9736 assert_eq!(scene_delta.new_graph.objects.len(), 3);
9737
9738 ctx.close().await;
9739 mock_ctx.close().await;
9740 }
9741
9742 #[tokio::test(flavor = "multi_thread")]
9743 async fn test_delete_coincident_constraint() {
9744 let initial_source = "\
9745sketch(on = XY) {
9746 point1 = point(at = [var 1, var 2])
9747 point2 = point(at = [var 3, var 4])
9748 coincident([point1, point2])
9749 point(at = [var 5, var 6])
9750}
9751";
9752
9753 let program = Program::parse(initial_source).unwrap().0.unwrap();
9754
9755 let mut frontend = FrontendState::new();
9756
9757 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9758 let mock_ctx = ExecutorContext::new_mock(None).await;
9759 let version = Version(0);
9760
9761 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9762 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9763 let sketch_id = sketch_object.id;
9764 let sketch = expect_sketch(sketch_object);
9765
9766 let coincident_id = *sketch.constraints.first().unwrap();
9767
9768 let (src_delta, scene_delta) = frontend
9769 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9770 .await
9771 .unwrap();
9772 insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9773 assert_eq!(scene_delta.new_objects, vec![]);
9774 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9775
9776 ctx.close().await;
9777 mock_ctx.close().await;
9778 }
9779
9780 #[tokio::test(flavor = "multi_thread")]
9781 async fn test_delete_line_cascades_to_coincident_constraint() {
9782 let initial_source = "\
9783sketch(on = XY) {
9784 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9785 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9786 coincident([line1.end, line2.start])
9787}
9788";
9789
9790 let program = Program::parse(initial_source).unwrap().0.unwrap();
9791
9792 let mut frontend = FrontendState::new();
9793
9794 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9795 let mock_ctx = ExecutorContext::new_mock(None).await;
9796 let version = Version(0);
9797
9798 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9799 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9800 let sketch_id = sketch_object.id;
9801 let sketch = expect_sketch(sketch_object);
9802 let line_id = *sketch.segments.get(5).unwrap();
9803
9804 let (src_delta, scene_delta) = frontend
9805 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9806 .await
9807 .unwrap();
9808 insta::assert_snapshot!(
9809 "test_delete_line_cascades_to_coincident_constraint",
9810 src_delta.text.as_str()
9811 );
9812 assert_eq!(
9813 scene_delta.new_graph.objects.len(),
9814 5,
9815 "{:#?}",
9816 scene_delta.new_graph.objects
9817 );
9818
9819 ctx.close().await;
9820 mock_ctx.close().await;
9821 }
9822
9823 #[tokio::test(flavor = "multi_thread")]
9824 async fn test_delete_line_cascades_to_distance_constraint() {
9825 let initial_source = "\
9826sketch(on = XY) {
9827 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9828 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9829 distance([line1.end, line2.start]) == 10mm
9830}
9831";
9832
9833 let program = Program::parse(initial_source).unwrap().0.unwrap();
9834
9835 let mut frontend = FrontendState::new();
9836
9837 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9838 let mock_ctx = ExecutorContext::new_mock(None).await;
9839 let version = Version(0);
9840
9841 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9842 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9843 let sketch_id = sketch_object.id;
9844 let sketch = expect_sketch(sketch_object);
9845 let line_id = *sketch.segments.get(5).unwrap();
9846
9847 let (src_delta, scene_delta) = frontend
9848 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9849 .await
9850 .unwrap();
9851 insta::assert_snapshot!(
9852 "test_delete_line_cascades_to_distance_constraint",
9853 src_delta.text.as_str()
9854 );
9855 assert_eq!(
9856 scene_delta.new_graph.objects.len(),
9857 5,
9858 "{:#?}",
9859 scene_delta.new_graph.objects
9860 );
9861
9862 ctx.close().await;
9863 mock_ctx.close().await;
9864 }
9865
9866 #[tokio::test(flavor = "multi_thread")]
9867 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9868 let initial_source = "\
9869sketch(on = XY) {
9870 point1 = point(at = [var 1, var 2])
9871 point2 = point(at = [var 3, var 4])
9872 horizontalDistance([point1, point2]) == 10mm
9873}
9874";
9875
9876 let program = Program::parse(initial_source).unwrap().0.unwrap();
9877
9878 let mut frontend = FrontendState::new();
9879
9880 let mock_ctx = ExecutorContext::new_mock(None).await;
9881 let version = Version(0);
9882
9883 frontend.program = program.clone();
9884 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9885 frontend.update_state_after_exec(outcome, true);
9886 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9887 let sketch_id = sketch_object.id;
9888 let sketch = expect_sketch(sketch_object);
9889 let point2_id = *sketch.segments.get(1).unwrap();
9890
9891 let (src_delta, scene_delta) = frontend
9892 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9893 .await
9894 .unwrap();
9895 insta::assert_snapshot!(
9896 "test_delete_point_cascades_to_horizontal_distance_constraint",
9897 src_delta.text.as_str()
9898 );
9899 assert_eq!(
9900 scene_delta.new_graph.objects.len(),
9901 3,
9902 "{:#?}",
9903 scene_delta.new_graph.objects
9904 );
9905
9906 mock_ctx.close().await;
9907 }
9908
9909 #[tokio::test(flavor = "multi_thread")]
9910 async fn test_delete_line_cascades_to_fixed_constraint() {
9911 let initial_source = "\
9912sketch(on = XY) {
9913 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9914 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9915 fixed([line1.start, [0, 0]])
9916}
9917";
9918
9919 let program = Program::parse(initial_source).unwrap().0.unwrap();
9920
9921 let mut frontend = FrontendState::new();
9922
9923 let mock_ctx = ExecutorContext::new_mock(None).await;
9924 let version = Version(0);
9925
9926 frontend.program = program.clone();
9927 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9928 frontend.update_state_after_exec(outcome, true);
9929 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9930 let sketch_id = sketch_object.id;
9931 let sketch = expect_sketch(sketch_object);
9932 let line1_id = *sketch.segments.get(2).unwrap();
9933
9934 let (src_delta, scene_delta) = frontend
9935 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9936 .await
9937 .unwrap();
9938 insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9939 assert_eq!(
9940 scene_delta.new_graph.objects.len(),
9941 5,
9942 "{:#?}",
9943 scene_delta.new_graph.objects
9944 );
9945
9946 mock_ctx.close().await;
9947 }
9948
9949 #[tokio::test(flavor = "multi_thread")]
9950 async fn test_delete_line_cascades_to_midpoint_constraint() {
9951 let initial_source = "\
9952sketch(on = XY) {
9953 point1 = point(at = [var 1, var 2])
9954 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9955 midpoint(line1, point = point1)
9956}
9957";
9958
9959 let program = Program::parse(initial_source).unwrap().0.unwrap();
9960
9961 let mut frontend = FrontendState::new();
9962
9963 let mock_ctx = ExecutorContext::new_mock(None).await;
9964 let version = Version(0);
9965
9966 frontend.program = program.clone();
9967 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9968 frontend.update_state_after_exec(outcome, true);
9969 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9970 let sketch_id = sketch_object.id;
9971 let sketch = expect_sketch(sketch_object);
9972 let line1_id = *sketch.segments.get(3).unwrap();
9973
9974 let (src_delta, scene_delta) = frontend
9975 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9976 .await
9977 .unwrap();
9978 insta::assert_snapshot!(
9979 "test_delete_line_cascades_to_midpoint_constraint",
9980 src_delta.text.as_str()
9981 );
9982 assert_eq!(
9983 scene_delta.new_graph.objects.len(),
9984 3,
9985 "{:#?}",
9986 scene_delta.new_graph.objects
9987 );
9988
9989 mock_ctx.close().await;
9990 }
9991
9992 #[tokio::test(flavor = "multi_thread")]
9993 async fn test_delete_point_preserves_multiline_coincident_constraint() {
9994 let initial_source = "\
9995sketch(on = XY) {
9996 point1 = point(at = [var 1, var 2])
9997 point2 = point(at = [var 3, var 4])
9998 point3 = point(at = [var 5, var 6])
9999 coincident([point1, point2, point3])
10000}
10001";
10002
10003 let program = Program::parse(initial_source).unwrap().0.unwrap();
10004
10005 let mut frontend = FrontendState::new();
10006
10007 let mock_ctx = ExecutorContext::new_mock(None).await;
10008 let version = Version(0);
10009
10010 frontend.program = program.clone();
10011 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10012 frontend.update_state_after_exec(outcome, true);
10013 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10014 let sketch_id = sketch_object.id;
10015 let sketch = expect_sketch(sketch_object);
10016 let point3_id = *sketch.segments.get(2).unwrap();
10017
10018 let (src_delta, scene_delta) = frontend
10019 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10020 .await
10021 .unwrap();
10022 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10023 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10024 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10025 assert!(
10026 src_delta.text.contains("coincident([point1, point2])"),
10027 "{}",
10028 src_delta.text
10029 );
10030
10031 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10032 let sketch = expect_sketch(sketch_object);
10033 assert_eq!(sketch.segments.len(), 2);
10034 assert_eq!(sketch.constraints.len(), 1);
10035
10036 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10037 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10038 panic!("Expected constraint object");
10039 };
10040 let Constraint::Coincident(coincident) = constraint else {
10041 panic!("Expected coincident constraint");
10042 };
10043 assert_eq!(
10044 coincident.segments,
10045 sketch
10046 .segments
10047 .iter()
10048 .copied()
10049 .map(Into::into)
10050 .collect::<Vec<ConstraintSegment>>()
10051 );
10052
10053 mock_ctx.close().await;
10054 }
10055
10056 #[tokio::test(flavor = "multi_thread")]
10057 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10058 let initial_source = "\
10059sketch(on = XY) {
10060 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10061 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10062 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10063 equalLength([line1, line2, line3])
10064}
10065";
10066
10067 let program = Program::parse(initial_source).unwrap().0.unwrap();
10068
10069 let mut frontend = FrontendState::new();
10070
10071 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10072 let mock_ctx = ExecutorContext::new_mock(None).await;
10073 let version = Version(0);
10074
10075 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10076 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10077 let sketch_id = sketch_object.id;
10078 let sketch = expect_sketch(sketch_object);
10079 let line3_id = *sketch.segments.get(8).unwrap();
10080
10081 let (src_delta, scene_delta) = frontend
10082 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10083 .await
10084 .unwrap();
10085 insta::assert_snapshot!(
10086 "test_delete_line_preserves_multiline_equal_length_constraint",
10087 src_delta.text.as_str()
10088 );
10089
10090 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10091 let sketch = expect_sketch(sketch_object);
10092 assert_eq!(sketch.constraints.len(), 1);
10093
10094 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10095 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10096 panic!("Expected constraint object");
10097 };
10098 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10099 panic!("Expected lines equal length constraint");
10100 };
10101 assert_eq!(lines_equal_length.lines.len(), 2);
10102
10103 ctx.close().await;
10104 mock_ctx.close().await;
10105 }
10106
10107 #[tokio::test(flavor = "multi_thread")]
10108 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10109 let initial_source = "\
10110sketch(on = XY) {
10111 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10112 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10113 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10114 horizontal([line1.end, line2.start, line3.start])
10115}
10116";
10117
10118 let program = Program::parse(initial_source).unwrap().0.unwrap();
10119
10120 let mut frontend = FrontendState::new();
10121
10122 let mock_ctx = ExecutorContext::new_mock(None).await;
10123 let version = Version(0);
10124
10125 frontend.program = program.clone();
10126 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10127 frontend.update_state_after_exec(outcome, true);
10128 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10129 let sketch_id = sketch_object.id;
10130 let sketch = expect_sketch(sketch_object);
10131 let line1_id = *sketch.segments.get(2).unwrap();
10132
10133 let (src_delta, scene_delta) = frontend
10134 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10135 .await
10136 .unwrap();
10137 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10138 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10139 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10140 assert!(
10141 src_delta.text.contains("horizontal([line2.start, line3.start])"),
10142 "{}",
10143 src_delta.text
10144 );
10145
10146 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10147 let sketch = expect_sketch(sketch_object);
10148 assert_eq!(sketch.constraints.len(), 1);
10149
10150 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10151 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10152 panic!("Expected constraint object");
10153 };
10154 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10155 panic!("Expected horizontal points constraint");
10156 };
10157 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10158 assert_eq!(*points, remaining_points);
10159
10160 mock_ctx.close().await;
10161 }
10162
10163 #[tokio::test(flavor = "multi_thread")]
10164 async fn test_delete_line_preserves_multiline_vertical_constraint() {
10165 let initial_source = "\
10166sketch(on = XY) {
10167 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10168 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10169 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10170 vertical([line1.end, line2.start, line3.start])
10171}
10172";
10173
10174 let program = Program::parse(initial_source).unwrap().0.unwrap();
10175
10176 let mut frontend = FrontendState::new();
10177
10178 let mock_ctx = ExecutorContext::new_mock(None).await;
10179 let version = Version(0);
10180
10181 frontend.program = program.clone();
10182 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10183 frontend.update_state_after_exec(outcome, true);
10184 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10185 let sketch_id = sketch_object.id;
10186 let sketch = expect_sketch(sketch_object);
10187 let line1_id = *sketch.segments.get(2).unwrap();
10188
10189 let (src_delta, scene_delta) = frontend
10190 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10191 .await
10192 .unwrap();
10193 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10194 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10195 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10196 assert!(
10197 src_delta.text.contains("vertical([line2.start, line3.start])"),
10198 "{}",
10199 src_delta.text
10200 );
10201
10202 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10203 let sketch = expect_sketch(sketch_object);
10204 assert_eq!(sketch.constraints.len(), 1);
10205
10206 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10207 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10208 panic!("Expected constraint object");
10209 };
10210 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10211 panic!("Expected vertical points constraint");
10212 };
10213 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10214 assert_eq!(*points, remaining_points);
10215
10216 mock_ctx.close().await;
10217 }
10218
10219 #[tokio::test(flavor = "multi_thread")]
10220 async fn test_delete_line_preserves_multiline_coincident_constraint() {
10221 let initial_source = "\
10222sketch(on = XY) {
10223 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10224 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10225 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10226 coincident([line1.end, line2.start, line3.start])
10227}
10228";
10229
10230 let program = Program::parse(initial_source).unwrap().0.unwrap();
10231
10232 let mut frontend = FrontendState::new();
10233
10234 let mock_ctx = ExecutorContext::new_mock(None).await;
10235 let version = Version(0);
10236
10237 frontend.program = program.clone();
10238 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10239 frontend.update_state_after_exec(outcome, true);
10240 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10241 let sketch_id = sketch_object.id;
10242 let sketch = expect_sketch(sketch_object);
10243 let line1_id = *sketch.segments.get(2).unwrap();
10244
10245 let (src_delta, scene_delta) = frontend
10246 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10247 .await
10248 .unwrap();
10249 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10250 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10251 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10252 assert!(
10253 src_delta.text.contains("coincident([line2.start, line3.start])"),
10254 "{}",
10255 src_delta.text
10256 );
10257
10258 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10259 let sketch = expect_sketch(sketch_object);
10260 assert_eq!(sketch.constraints.len(), 1);
10261
10262 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10263 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10264 panic!("Expected constraint object");
10265 };
10266 let Constraint::Coincident(coincident) = constraint else {
10267 panic!("Expected coincident constraint");
10268 };
10269 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10270 assert_eq!(coincident.segments, remaining_segments);
10271
10272 mock_ctx.close().await;
10273 }
10274
10275 #[tokio::test(flavor = "multi_thread")]
10276 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10277 let initial_source = "\
10278sketch(on = XY) {
10279 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10280 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10281 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10282 equalLength([line1, line2, line3])
10283}
10284";
10285
10286 let program = Program::parse(initial_source).unwrap().0.unwrap();
10287
10288 let mut frontend = FrontendState::new();
10289
10290 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10291 let mock_ctx = ExecutorContext::new_mock(None).await;
10292 let version = Version(0);
10293
10294 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10295 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10296 let sketch_id = sketch_object.id;
10297 let sketch = expect_sketch(sketch_object);
10298 let line2_id = *sketch.segments.get(5).unwrap();
10299 let line3_id = *sketch.segments.get(8).unwrap();
10300
10301 let (src_delta, scene_delta) = frontend
10302 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10303 .await
10304 .unwrap();
10305 insta::assert_snapshot!(
10306 "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10307 src_delta.text.as_str()
10308 );
10309
10310 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10311 let sketch = expect_sketch(sketch_object);
10312 assert!(sketch.constraints.is_empty());
10313
10314 ctx.close().await;
10315 mock_ctx.close().await;
10316 }
10317
10318 #[tokio::test(flavor = "multi_thread")]
10319 async fn test_delete_line_preserves_multiline_parallel_constraint() {
10320 let initial_source = "\
10321sketch(on = XY) {
10322 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10323 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10324 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10325 parallel([line1, line2, line3])
10326}
10327";
10328
10329 let program = Program::parse(initial_source).unwrap().0.unwrap();
10330
10331 let mut frontend = FrontendState::new();
10332
10333 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10334 let mock_ctx = ExecutorContext::new_mock(None).await;
10335 let version = Version(0);
10336
10337 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10338 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10339 let sketch_id = sketch_object.id;
10340 let sketch = expect_sketch(sketch_object);
10341 let line3_id = *sketch.segments.get(8).unwrap();
10342
10343 let (src_delta, scene_delta) = frontend
10344 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10345 .await
10346 .unwrap();
10347 insta::assert_snapshot!(
10348 "test_delete_line_preserves_multiline_parallel_constraint",
10349 src_delta.text.as_str()
10350 );
10351
10352 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10353 let sketch = expect_sketch(sketch_object);
10354 assert_eq!(sketch.constraints.len(), 1);
10355
10356 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10357 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10358 panic!("Expected constraint object");
10359 };
10360 let Constraint::Parallel(parallel) = constraint else {
10361 panic!("Expected parallel constraint");
10362 };
10363 assert_eq!(parallel.lines.len(), 2);
10364
10365 ctx.close().await;
10366 mock_ctx.close().await;
10367 }
10368
10369 #[tokio::test(flavor = "multi_thread")]
10370 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10371 let initial_source = "\
10372sketch(on = XY) {
10373 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10374 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10375 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10376 parallel([line1, line2, line3])
10377}
10378";
10379
10380 let program = Program::parse(initial_source).unwrap().0.unwrap();
10381
10382 let mut frontend = FrontendState::new();
10383
10384 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10385 let mock_ctx = ExecutorContext::new_mock(None).await;
10386 let version = Version(0);
10387
10388 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10389 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10390 let sketch_id = sketch_object.id;
10391 let sketch = expect_sketch(sketch_object);
10392 let line2_id = *sketch.segments.get(5).unwrap();
10393 let line3_id = *sketch.segments.get(8).unwrap();
10394
10395 let (src_delta, scene_delta) = frontend
10396 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10397 .await
10398 .unwrap();
10399 insta::assert_snapshot!(
10400 "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10401 src_delta.text.as_str()
10402 );
10403
10404 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10405 let sketch = expect_sketch(sketch_object);
10406 assert!(sketch.constraints.is_empty());
10407
10408 ctx.close().await;
10409 mock_ctx.close().await;
10410 }
10411
10412 #[tokio::test(flavor = "multi_thread")]
10413 async fn test_delete_line_line_coincident_constraint() {
10414 let initial_source = "\
10415sketch(on = XY) {
10416 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10417 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10418 coincident([line1, line2])
10419}
10420";
10421
10422 let program = Program::parse(initial_source).unwrap().0.unwrap();
10423
10424 let mut frontend = FrontendState::new();
10425
10426 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10427 let mock_ctx = ExecutorContext::new_mock(None).await;
10428 let version = Version(0);
10429
10430 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10431 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10432 let sketch_id = sketch_object.id;
10433 let sketch = expect_sketch(sketch_object);
10434
10435 let coincident_id = *sketch.constraints.first().unwrap();
10436
10437 let (src_delta, scene_delta) = frontend
10438 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10439 .await
10440 .unwrap();
10441 insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10442 assert_eq!(scene_delta.new_objects, vec![]);
10443 assert_eq!(scene_delta.new_graph.objects.len(), 8);
10444
10445 ctx.close().await;
10446 mock_ctx.close().await;
10447 }
10448
10449 #[tokio::test(flavor = "multi_thread")]
10450 async fn test_two_points_coincident() {
10451 let initial_source = "\
10452sketch(on = XY) {
10453 point1 = point(at = [var 1, var 2])
10454 point(at = [3, 4])
10455}
10456";
10457
10458 let program = Program::parse(initial_source).unwrap().0.unwrap();
10459
10460 let mut frontend = FrontendState::new();
10461
10462 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10463 let mock_ctx = ExecutorContext::new_mock(None).await;
10464 let version = Version(0);
10465
10466 frontend.hack_set_program(&ctx, program).await.unwrap();
10467 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10468 let sketch_id = sketch_object.id;
10469 let sketch = expect_sketch(sketch_object);
10470 let point0_id = *sketch.segments.first().unwrap();
10471 let point1_id = *sketch.segments.get(1).unwrap();
10472
10473 let constraint = Constraint::Coincident(Coincident {
10474 segments: vec![point0_id.into(), point1_id.into()],
10475 });
10476 let (src_delta, scene_delta) = frontend
10477 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10478 .await
10479 .unwrap();
10480 insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10481 assert_eq!(
10482 scene_delta.new_graph.objects.len(),
10483 5,
10484 "{:#?}",
10485 scene_delta.new_graph.objects
10486 );
10487
10488 ctx.close().await;
10489 mock_ctx.close().await;
10490 }
10491
10492 #[tokio::test(flavor = "multi_thread")]
10493 async fn test_three_points_coincident() {
10494 let initial_source = "\
10495sketch(on = XY) {
10496 point1 = point(at = [var 1, var 2])
10497 point(at = [var 3, var 4])
10498 point(at = [var 5, var 6])
10499}
10500";
10501
10502 let program = Program::parse(initial_source).unwrap().0.unwrap();
10503
10504 let mut frontend = FrontendState::new();
10505
10506 let mock_ctx = ExecutorContext::new_mock(None).await;
10507 let version = Version(0);
10508
10509 frontend.program = program.clone();
10510 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10511 frontend.update_state_after_exec(outcome, true);
10512 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10513 let sketch_id = sketch_object.id;
10514 let sketch = expect_sketch(sketch_object);
10515 let segments = sketch
10516 .segments
10517 .iter()
10518 .take(3)
10519 .copied()
10520 .map(Into::into)
10521 .collect::<Vec<ConstraintSegment>>();
10522
10523 let constraint = Constraint::Coincident(Coincident {
10524 segments: segments.clone(),
10525 });
10526 let (src_delta, scene_delta) = frontend
10527 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10528 .await
10529 .unwrap();
10530 insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10531
10532 let constraint_object = scene_delta
10533 .new_graph
10534 .objects
10535 .iter()
10536 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10537 .unwrap();
10538
10539 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10540 panic!("expected a constraint object");
10541 };
10542
10543 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10544
10545 mock_ctx.close().await;
10546 }
10547
10548 #[tokio::test(flavor = "multi_thread")]
10549 async fn test_source_with_three_point_coincident_tracks_all_segments() {
10550 let initial_source = "\
10551sketch(on = XY) {
10552 point1 = point(at = [var 1, var 2])
10553 point2 = point(at = [var 3, var 4])
10554 point3 = point(at = [var 5, var 6])
10555 coincident([point1, point2, point3])
10556}
10557";
10558
10559 let program = Program::parse(initial_source).unwrap().0.unwrap();
10560
10561 let mut frontend = FrontendState::new();
10562
10563 let ctx = ExecutorContext::new_mock(None).await;
10564 frontend.program = program.clone();
10565 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10566 frontend.update_state_after_exec(outcome, true);
10567
10568 let constraint_object = frontend
10569 .scene_graph
10570 .objects
10571 .iter()
10572 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10573 .unwrap();
10574 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10575 panic!("expected a constraint object");
10576 };
10577
10578 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10579 let sketch = expect_sketch(sketch_object);
10580 let expected_segments = sketch
10581 .segments
10582 .iter()
10583 .take(3)
10584 .copied()
10585 .map(Into::into)
10586 .collect::<Vec<ConstraintSegment>>();
10587
10588 assert_eq!(
10589 constraint,
10590 &Constraint::Coincident(Coincident {
10591 segments: expected_segments,
10592 })
10593 );
10594
10595 ctx.close().await;
10596 }
10597
10598 #[tokio::test(flavor = "multi_thread")]
10599 async fn test_point_origin_coincident_preserves_order() {
10600 let initial_source = "\
10601sketch(on = XY) {
10602 point(at = [var 1, var 2])
10603}
10604";
10605
10606 for (origin_first, snapshot_name) in [
10607 (true, "test_point_origin_coincident_preserves_order_origin_first"),
10608 (false, "test_point_origin_coincident_preserves_order_point_first"),
10609 ] {
10610 let program = Program::parse(initial_source).unwrap().0.unwrap();
10611
10612 let mut frontend = FrontendState::new();
10613
10614 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10615 let mock_ctx = ExecutorContext::new_mock(None).await;
10616 let version = Version(0);
10617
10618 frontend.hack_set_program(&ctx, program).await.unwrap();
10619 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10620 let sketch_id = sketch_object.id;
10621 let sketch = expect_sketch(sketch_object);
10622 let point_id = *sketch.segments.first().unwrap();
10623
10624 let segments = if origin_first {
10625 vec![ConstraintSegment::ORIGIN, point_id.into()]
10626 } else {
10627 vec![point_id.into(), ConstraintSegment::ORIGIN]
10628 };
10629 let constraint = Constraint::Coincident(Coincident {
10630 segments: segments.clone(),
10631 });
10632 let (src_delta, scene_delta) = frontend
10633 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10634 .await
10635 .unwrap();
10636 insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10637
10638 let constraint_object = scene_delta
10639 .new_graph
10640 .objects
10641 .iter()
10642 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10643 .unwrap();
10644
10645 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10646 panic!("expected a constraint object");
10647 };
10648
10649 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10650
10651 ctx.close().await;
10652 mock_ctx.close().await;
10653 }
10654 }
10655
10656 #[tokio::test(flavor = "multi_thread")]
10657 async fn test_coincident_of_line_end_points() {
10658 let initial_source = "\
10659sketch(on = XY) {
10660 line(start = [var 1, var 2], end = [var 3, var 4])
10661 line(start = [var 5, var 6], end = [var 7, var 8])
10662}
10663";
10664
10665 let program = Program::parse(initial_source).unwrap().0.unwrap();
10666
10667 let mut frontend = FrontendState::new();
10668
10669 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10670 let mock_ctx = ExecutorContext::new_mock(None).await;
10671 let version = Version(0);
10672
10673 frontend.hack_set_program(&ctx, program).await.unwrap();
10674 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10675 let sketch_id = sketch_object.id;
10676 let sketch = expect_sketch(sketch_object);
10677 let point0_id = *sketch.segments.get(1).unwrap();
10678 let point1_id = *sketch.segments.get(3).unwrap();
10679
10680 let constraint = Constraint::Coincident(Coincident {
10681 segments: vec![point0_id.into(), point1_id.into()],
10682 });
10683 let (src_delta, scene_delta) = frontend
10684 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10685 .await
10686 .unwrap();
10687 insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10688 assert_eq!(
10689 scene_delta.new_graph.objects.len(),
10690 9,
10691 "{:#?}",
10692 scene_delta.new_graph.objects
10693 );
10694
10695 ctx.close().await;
10696 mock_ctx.close().await;
10697 }
10698
10699 #[tokio::test(flavor = "multi_thread")]
10700 async fn test_coincident_of_line_point_and_circle_segment() {
10701 let initial_source = "\
10702sketch(on = XY) {
10703 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10704 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10705}
10706";
10707 let program = Program::parse(initial_source).unwrap().0.unwrap();
10708 let mut frontend = FrontendState::new();
10709
10710 let mock_ctx = ExecutorContext::new_mock(None).await;
10711 let version = Version(0);
10712
10713 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10714 frontend.program = program;
10715 frontend.update_state_after_exec(outcome, true);
10716 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10717 let sketch_id = sketch_object.id;
10718 let sketch = expect_sketch(sketch_object);
10719
10720 let circle_id = sketch
10721 .segments
10722 .iter()
10723 .copied()
10724 .find(|seg_id| {
10725 matches!(
10726 &frontend.scene_graph.objects[seg_id.0].kind,
10727 ObjectKind::Segment {
10728 segment: Segment::Circle(_)
10729 }
10730 )
10731 })
10732 .expect("Expected a circle segment in sketch");
10733 let line_id = frontend
10734 .scene_graph
10735 .objects
10736 .iter()
10737 .find_map(|obj| match &obj.kind {
10738 ObjectKind::Segment {
10739 segment: Segment::Line(line),
10740 } if line.owner.is_none() => Some(obj.id),
10741 _ => None,
10742 })
10743 .expect("Expected a standalone line segment in scene graph");
10744
10745 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10746 ObjectKind::Segment {
10747 segment: Segment::Line(line),
10748 } => line.start,
10749 _ => panic!("Expected line segment object"),
10750 };
10751
10752 let constraint = Constraint::Coincident(Coincident {
10753 segments: vec![line_start_point_id.into(), circle_id.into()],
10754 });
10755 let (src_delta, _scene_delta) = frontend
10756 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10757 .await
10758 .unwrap();
10759 insta::assert_snapshot!(
10760 "test_coincident_of_line_point_and_circle_segment",
10761 src_delta.text.as_str()
10762 );
10763
10764 mock_ctx.close().await;
10765 }
10766
10767 #[tokio::test(flavor = "multi_thread")]
10768 async fn test_invalid_coincident_arc_and_line_preserves_state() {
10769 let program = Program::empty();
10777
10778 let mut frontend = FrontendState::new();
10779 frontend.program = program;
10780
10781 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10782 let mock_ctx = ExecutorContext::new_mock(None).await;
10783 let version = Version(0);
10784
10785 let sketch_args = SketchCtor {
10786 on: Plane::Default(PlaneName::Xy),
10787 };
10788 let (_src_delta, _scene_delta, sketch_id) = frontend
10789 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10790 .await
10791 .unwrap();
10792
10793 let arc_ctor = ArcCtor {
10795 start: Point2d {
10796 x: Expr::Var(Number {
10797 value: 0.0,
10798 units: NumericSuffix::Mm,
10799 }),
10800 y: Expr::Var(Number {
10801 value: 0.0,
10802 units: NumericSuffix::Mm,
10803 }),
10804 },
10805 end: Point2d {
10806 x: Expr::Var(Number {
10807 value: 10.0,
10808 units: NumericSuffix::Mm,
10809 }),
10810 y: Expr::Var(Number {
10811 value: 10.0,
10812 units: NumericSuffix::Mm,
10813 }),
10814 },
10815 center: Point2d {
10816 x: Expr::Var(Number {
10817 value: 10.0,
10818 units: NumericSuffix::Mm,
10819 }),
10820 y: Expr::Var(Number {
10821 value: 0.0,
10822 units: NumericSuffix::Mm,
10823 }),
10824 },
10825 direction: None,
10826 construction: None,
10827 };
10828 let (_src_delta, scene_delta) = frontend
10829 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10830 .await
10831 .unwrap();
10832 let arc_id = *scene_delta.new_objects.last().unwrap();
10834
10835 let line_ctor = LineCtor {
10837 start: Point2d {
10838 x: Expr::Var(Number {
10839 value: 20.0,
10840 units: NumericSuffix::Mm,
10841 }),
10842 y: Expr::Var(Number {
10843 value: 0.0,
10844 units: NumericSuffix::Mm,
10845 }),
10846 },
10847 end: Point2d {
10848 x: Expr::Var(Number {
10849 value: 30.0,
10850 units: NumericSuffix::Mm,
10851 }),
10852 y: Expr::Var(Number {
10853 value: 10.0,
10854 units: NumericSuffix::Mm,
10855 }),
10856 },
10857 construction: None,
10858 };
10859 let (_src_delta, scene_delta) = frontend
10860 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10861 .await
10862 .unwrap();
10863 let line_id = *scene_delta.new_objects.last().unwrap();
10865
10866 let constraint = Constraint::Coincident(Coincident {
10869 segments: vec![arc_id.into(), line_id.into()],
10870 });
10871 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10872
10873 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10875
10876 let sketch_object_after =
10879 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10880 let sketch_after = expect_sketch(sketch_object_after);
10881
10882 assert!(
10884 sketch_after.segments.contains(&arc_id),
10885 "Arc segment should still exist after failed constraint"
10886 );
10887 assert!(
10888 sketch_after.segments.contains(&line_id),
10889 "Line segment should still exist after failed constraint"
10890 );
10891
10892 let arc_obj = frontend
10894 .scene_graph
10895 .objects
10896 .get(arc_id.0)
10897 .expect("Arc object should still be accessible");
10898 let line_obj = frontend
10899 .scene_graph
10900 .objects
10901 .get(line_id.0)
10902 .expect("Line object should still be accessible");
10903
10904 match &arc_obj.kind {
10907 ObjectKind::Segment {
10908 segment: Segment::Arc(_),
10909 } => {}
10910 _ => panic!("Arc object should still be an arc segment"),
10911 }
10912 match &line_obj.kind {
10913 ObjectKind::Segment {
10914 segment: Segment::Line(_),
10915 } => {}
10916 _ => panic!("Line object should still be a line segment"),
10917 }
10918
10919 ctx.close().await;
10920 mock_ctx.close().await;
10921 }
10922
10923 #[tokio::test(flavor = "multi_thread")]
10924 async fn test_distance_two_points() {
10925 let initial_source = "\
10926sketch(on = XY) {
10927 point(at = [var 1, var 2])
10928 point(at = [var 3, var 4])
10929}
10930";
10931
10932 let program = Program::parse(initial_source).unwrap().0.unwrap();
10933
10934 let mut frontend = FrontendState::new();
10935
10936 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10937 let mock_ctx = ExecutorContext::new_mock(None).await;
10938 let version = Version(0);
10939
10940 frontend.hack_set_program(&ctx, program).await.unwrap();
10941 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10942 let sketch_id = sketch_object.id;
10943 let sketch = expect_sketch(sketch_object);
10944 let point0_id = *sketch.segments.first().unwrap();
10945 let point1_id = *sketch.segments.get(1).unwrap();
10946
10947 let constraint = Constraint::Distance(Distance {
10948 segments: vec![point0_id.into(), point1_id.into()],
10949 distance: Number {
10950 value: 2.0,
10951 units: NumericSuffix::Mm,
10952 },
10953 label_position: None,
10954 source: Default::default(),
10955 });
10956 let (src_delta, scene_delta) = frontend
10957 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10958 .await
10959 .unwrap();
10960 insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
10961 assert_eq!(
10962 scene_delta.new_graph.objects.len(),
10963 5,
10964 "{:#?}",
10965 scene_delta.new_graph.objects
10966 );
10967
10968 ctx.close().await;
10969 mock_ctx.close().await;
10970 }
10971
10972 #[tokio::test(flavor = "multi_thread")]
10973 async fn test_distance_two_points_with_label() {
10974 let initial_source = "\
10975sketch(on = XY) {
10976 point(at = [var 1, var 2])
10977 point(at = [var 3, var 4])
10978}
10979";
10980
10981 let program = Program::parse(initial_source).unwrap().0.unwrap();
10982
10983 let mut frontend = FrontendState::new();
10984
10985 let mock_ctx = ExecutorContext::new_mock(None).await;
10986 let version = Version(0);
10987
10988 frontend.program = program.clone();
10989 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10990 frontend.update_state_after_exec(outcome, true);
10991 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10992 let sketch_id = sketch_object.id;
10993 let sketch = expect_sketch(sketch_object);
10994 let point0_id = *sketch.segments.first().unwrap();
10995 let point1_id = *sketch.segments.get(1).unwrap();
10996
10997 let label_position = Point2d {
10998 x: Number {
10999 value: 10.0,
11000 units: NumericSuffix::Mm,
11001 },
11002 y: Number {
11003 value: 11.0,
11004 units: NumericSuffix::Mm,
11005 },
11006 };
11007 let constraint = Constraint::Distance(Distance {
11008 segments: vec![point0_id.into(), point1_id.into()],
11009 distance: Number {
11010 value: 2.0,
11011 units: NumericSuffix::Mm,
11012 },
11013 label_position: Some(label_position.clone()),
11014 source: Default::default(),
11015 });
11016 let (src_delta, scene_delta) = frontend
11017 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11018 .await
11019 .unwrap();
11020 insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11021
11022 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11023 let sketch = expect_sketch(sketch_object);
11024 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11025 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11026 panic!("Expected constraint object");
11027 };
11028 let Constraint::Distance(distance) = constraint else {
11029 panic!("Expected distance constraint");
11030 };
11031 assert_eq!(distance.label_position, Some(label_position));
11032
11033 mock_ctx.close().await;
11034 }
11035
11036 #[tokio::test(flavor = "multi_thread")]
11037 async fn test_edit_distance_constraint_label_position() {
11038 let initial_source = "\
11039sketch(on = XY) {
11040 point(at = [var 1, var 2])
11041 point(at = [var 3, var 2])
11042}
11043";
11044
11045 let program = Program::parse(initial_source).unwrap().0.unwrap();
11046
11047 let mut frontend = FrontendState::new();
11048
11049 let mock_ctx = ExecutorContext::new_mock(None).await;
11050 let version = Version(0);
11051
11052 frontend.program = program.clone();
11053 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11054 frontend.update_state_after_exec(outcome, true);
11055 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11056 let sketch_id = sketch_object.id;
11057 let sketch = expect_sketch(sketch_object);
11058 let point0_id = *sketch.segments.first().unwrap();
11059 let point1_id = *sketch.segments.get(1).unwrap();
11060
11061 let constraint = Constraint::Distance(Distance {
11062 segments: vec![point0_id.into(), point1_id.into()],
11063 distance: Number {
11064 value: 2.0,
11065 units: NumericSuffix::Mm,
11066 },
11067 label_position: None,
11068 source: Default::default(),
11069 });
11070 let (_, scene_delta) = frontend
11071 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11072 .await
11073 .unwrap();
11074 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11075 let sketch = expect_sketch(sketch_object);
11076 let constraint_id = sketch.constraints[0];
11077 let label_position = Point2d {
11078 x: Number {
11079 value: 10.0,
11080 units: NumericSuffix::Mm,
11081 },
11082 y: Number {
11083 value: 11.0,
11084 units: NumericSuffix::Mm,
11085 },
11086 };
11087
11088 let (src_delta, scene_delta) = frontend
11089 .edit_distance_constraint_label_position(
11090 &mock_ctx,
11091 version,
11092 sketch_id,
11093 constraint_id,
11094 label_position.clone(),
11095 vec![],
11096 )
11097 .await
11098 .unwrap();
11099 insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11100
11101 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11102 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11103 panic!("Expected constraint object");
11104 };
11105 let Constraint::Distance(distance) = constraint else {
11106 panic!("Expected distance constraint");
11107 };
11108 assert_eq!(distance.label_position, Some(label_position));
11109
11110 mock_ctx.close().await;
11111 }
11112
11113 #[tokio::test(flavor = "multi_thread")]
11114 async fn test_edit_distance_constraint_type_and_value() {
11115 let initial_source = "\
11116sketch(on = XY) {
11117 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11118 distance([line1.start, line1.end]) == 5mm
11119}
11120";
11121
11122 let program = Program::parse(initial_source).unwrap().0.unwrap();
11123 let mut frontend = FrontendState::new();
11124 let mock_ctx = ExecutorContext::new_mock(None).await;
11125 let version = Version(0);
11126
11127 frontend.program = program.clone();
11128 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11129 frontend.update_state_after_exec(outcome, true);
11130 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11131 let sketch_id = sketch_object.id;
11132 let sketch = expect_sketch(sketch_object);
11133 let constraint_id = sketch.constraints[0];
11134 let point0_id = sketch.segments[0];
11135 let point1_id = sketch.segments[1];
11136 let label_position = Point2d {
11137 x: Number {
11138 value: 2.0,
11139 units: NumericSuffix::Mm,
11140 },
11141 y: Number {
11142 value: 5.0,
11143 units: NumericSuffix::Mm,
11144 },
11145 };
11146
11147 let (source_delta, scene_delta) = frontend
11148 .edit_distance_constraint_with_options(
11149 &mock_ctx,
11150 version,
11151 sketch_id,
11152 constraint_id,
11153 Constraint::HorizontalDistance(Distance {
11154 segments: vec![point0_id.into(), point1_id.into()],
11155 distance: Number {
11156 value: 4.0,
11157 units: NumericSuffix::Mm,
11158 },
11159 label_position: Some(label_position.clone()),
11160 source: Default::default(),
11161 }),
11162 EditConstraintOptions {
11163 commit_solved_initial_guesses: false,
11164 },
11165 )
11166 .await
11167 .unwrap();
11168 assert_eq!(
11169 source_delta.text,
11170 "\
11171sketch(on = XY) {
11172 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11173 horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11174}
11175"
11176 );
11177
11178 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11179 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11180 panic!("Expected constraint object");
11181 };
11182 let Constraint::HorizontalDistance(distance) = constraint else {
11183 panic!("Expected horizontal distance constraint");
11184 };
11185 assert_eq!(distance.distance.value, 4.0);
11186 assert_eq!(distance.label_position, Some(label_position));
11187
11188 mock_ctx.close().await;
11189 }
11190
11191 #[tokio::test(flavor = "multi_thread")]
11192 async fn test_edit_angle_constraint_label_position() {
11193 let initial_source = "\
11194sketch(on = XY) {
11195 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11196 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11197 angle([line1, line2]) == 60deg
11198}
11199";
11200
11201 let program = Program::parse(initial_source).unwrap().0.unwrap();
11202 let mut frontend = FrontendState::new();
11203 let mock_ctx = ExecutorContext::new_mock(None).await;
11204 let version = Version(0);
11205
11206 frontend.program = program.clone();
11207 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11208 frontend.update_state_after_exec(outcome, true);
11209 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11210 let sketch_id = sketch_object.id;
11211 let sketch = expect_sketch(sketch_object);
11212 let constraint_id = sketch.constraints[0];
11213 let label_position = Point2d {
11214 x: Number {
11215 value: 10.0,
11216 units: NumericSuffix::Mm,
11217 },
11218 y: Number {
11219 value: 11.0,
11220 units: NumericSuffix::Mm,
11221 },
11222 };
11223
11224 let (src_delta, scene_delta) = frontend
11225 .edit_distance_constraint_label_position(
11226 &mock_ctx,
11227 version,
11228 sketch_id,
11229 constraint_id,
11230 label_position.clone(),
11231 vec![],
11232 )
11233 .await
11234 .unwrap();
11235 assert_eq!(
11236 src_delta.text.as_str(),
11237 "\
11238sketch(on = XY) {
11239 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11240 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11241 angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11242}
11243"
11244 );
11245
11246 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11247 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11248 panic!("Expected constraint object");
11249 };
11250 let Constraint::Angle(angle) = constraint else {
11251 panic!("Expected angle constraint");
11252 };
11253 assert_eq!(angle.label_position, Some(label_position));
11254
11255 mock_ctx.close().await;
11256 }
11257
11258 #[tokio::test(flavor = "multi_thread")]
11259 async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11260 let initial_source = "\
11261sketch(on = XY) {
11262 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11263 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11264 60deg == angleDimension(lines = [line1, line2], sector = 1)
11265}
11266";
11267
11268 let program = Program::parse(initial_source).unwrap().0.unwrap();
11269 let mut frontend = FrontendState::new();
11270 let mock_ctx = ExecutorContext::new_mock(None).await;
11271 let version = Version(0);
11272
11273 frontend.program = program.clone();
11274 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11275 frontend.update_state_after_exec(outcome, true);
11276 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11277 let sketch_id = sketch_object.id;
11278 let sketch = expect_sketch(sketch_object);
11279 let constraint_id = sketch.constraints[0];
11280 let label_position = Point2d {
11281 x: Number {
11282 value: 10.0,
11283 units: NumericSuffix::Mm,
11284 },
11285 y: Number {
11286 value: 11.0,
11287 units: NumericSuffix::Mm,
11288 },
11289 };
11290
11291 let (src_delta, scene_delta) = frontend
11292 .edit_distance_constraint_label_position(
11293 &mock_ctx,
11294 version,
11295 sketch_id,
11296 constraint_id,
11297 label_position.clone(),
11298 vec![],
11299 )
11300 .await
11301 .unwrap();
11302 assert_eq!(
11303 src_delta.text.as_str(),
11304 "\
11305sketch(on = XY) {
11306 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11307 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11308 60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11309}
11310"
11311 );
11312
11313 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11314 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11315 panic!("Expected constraint object");
11316 };
11317 let Constraint::Angle(angle) = constraint else {
11318 panic!("Expected angle constraint");
11319 };
11320 assert_eq!(angle.label_position, Some(label_position));
11321
11322 mock_ctx.close().await;
11323 }
11324
11325 #[tokio::test(flavor = "multi_thread")]
11326 async fn test_edit_angle_constraint() {
11327 let initial_source = "\
11328sketch(on = XY) {
11329 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11330 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11331 angle([line1, line2]) == 60deg
11332}
11333";
11334
11335 let program = Program::parse(initial_source).unwrap().0.unwrap();
11336 let mut frontend = FrontendState::new();
11337 let mock_ctx = ExecutorContext::new_mock(None).await;
11338 let version = Version(0);
11339
11340 frontend.program = program.clone();
11341 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11342 frontend.update_state_after_exec(outcome, true);
11343 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11344 let sketch_id = sketch_object.id;
11345 let sketch = expect_sketch(sketch_object);
11346 let constraint_id = sketch.constraints[0];
11347 let line1_id = *sketch.segments.get(2).unwrap();
11348 let line2_id = *sketch.segments.get(5).unwrap();
11349 let label_position = Point2d {
11350 x: Number {
11351 value: 10.0,
11352 units: NumericSuffix::Mm,
11353 },
11354 y: Number {
11355 value: 11.0,
11356 units: NumericSuffix::Mm,
11357 },
11358 };
11359
11360 let (src_delta, scene_delta) = frontend
11361 .edit_angle_constraint_with_options(
11362 &mock_ctx,
11363 version,
11364 sketch_id,
11365 constraint_id,
11366 Angle {
11367 lines: vec![line2_id, line1_id],
11368 angle: Number {
11369 value: 60.0,
11370 units: NumericSuffix::Deg,
11371 },
11372 sector: Some(3),
11373 inverse: Some(false),
11374 label_position: Some(label_position.clone()),
11375 source: Default::default(),
11376 },
11377 EditConstraintOptions {
11378 commit_solved_initial_guesses: false,
11379 },
11380 )
11381 .await
11382 .unwrap();
11383 assert_eq!(
11384 src_delta.text.as_str(),
11385 "\
11386sketch(on = XY) {
11387 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11388 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11389 angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11390}
11391"
11392 );
11393
11394 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11395 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11396 panic!("Expected constraint object");
11397 };
11398 let Constraint::Angle(angle) = constraint else {
11399 panic!("Expected angle constraint");
11400 };
11401 assert_eq!(angle.lines, vec![line2_id, line1_id]);
11402 assert_eq!(angle.sector, Some(3));
11403 assert_eq!(angle.inverse, Some(false));
11404 assert_eq!(angle.label_position, Some(label_position));
11405
11406 mock_ctx.close().await;
11407 }
11408
11409 #[tokio::test(flavor = "multi_thread")]
11410 async fn test_edit_angle_constraint_with_call_on_right() {
11411 let initial_source = "\
11412sketch(on = XY) {
11413 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11414 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11415 60deg == angle([line1, line2])
11416}
11417";
11418
11419 let program = Program::parse(initial_source).unwrap().0.unwrap();
11420 let mut frontend = FrontendState::new();
11421 let mock_ctx = ExecutorContext::new_mock(None).await;
11422 let version = Version(0);
11423
11424 frontend.program = program.clone();
11425 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11426 frontend.update_state_after_exec(outcome, true);
11427 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11428 let sketch_id = sketch_object.id;
11429 let sketch = expect_sketch(sketch_object);
11430 let constraint_id = sketch.constraints[0];
11431 let line1_id = *sketch.segments.get(2).unwrap();
11432 let line2_id = *sketch.segments.get(5).unwrap();
11433
11434 let (src_delta, _) = frontend
11435 .edit_angle_constraint_with_options(
11436 &mock_ctx,
11437 version,
11438 sketch_id,
11439 constraint_id,
11440 Angle {
11441 lines: vec![line2_id, line1_id],
11442 angle: Number {
11443 value: 60.0,
11444 units: NumericSuffix::Deg,
11445 },
11446 sector: Some(3),
11447 inverse: Some(false),
11448 label_position: None,
11449 source: Default::default(),
11450 },
11451 EditConstraintOptions {
11452 commit_solved_initial_guesses: false,
11453 },
11454 )
11455 .await
11456 .unwrap();
11457 assert_eq!(
11458 src_delta.text.as_str(),
11459 "\
11460sketch(on = XY) {
11461 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11462 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11463 60deg == angleDimension(lines = [line2, line1], sector = 3)
11464}
11465"
11466 );
11467
11468 mock_ctx.close().await;
11469 }
11470
11471 #[tokio::test(flavor = "multi_thread")]
11472 async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11473 let initial_source = "\
11474@settings(kclVersion = 2.0)
11475
11476sketch001 = sketch(on = XZ) {
11477 line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11478 line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11479 distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11480 vertical([line1.start, ORIGIN])
11481}
11482";
11483
11484 let program = Program::parse(initial_source).unwrap().0.unwrap();
11485 let mut frontend = FrontendState::new();
11486 let mock_ctx = ExecutorContext::new_mock(None).await;
11487 let version = Version(0);
11488
11489 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11490 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11491 let sketch_id = sketch_object.id;
11492 let sketch = expect_sketch(sketch_object);
11493 let constraint_id = sketch
11494 .constraints
11495 .iter()
11496 .copied()
11497 .find(|constraint_id| {
11498 matches!(
11499 frontend.scene_graph.objects[constraint_id.0].kind,
11500 ObjectKind::Constraint {
11501 constraint: Constraint::Distance(_)
11502 }
11503 )
11504 })
11505 .unwrap();
11506 let line1_id = sketch
11507 .segments
11508 .iter()
11509 .copied()
11510 .find(|segment_id| {
11511 matches!(
11512 frontend.scene_graph.objects[segment_id.0].kind,
11513 ObjectKind::Segment {
11514 segment: Segment::Line(_)
11515 }
11516 )
11517 })
11518 .unwrap();
11519 let label_position = Point2d {
11520 x: Number {
11521 value: 7.0,
11522 units: NumericSuffix::Mm,
11523 },
11524 y: Number {
11525 value: 9.0,
11526 units: NumericSuffix::Mm,
11527 },
11528 };
11529
11530 let (source_delta, scene_delta) = frontend
11531 .edit_segments_with_options(
11532 &mock_ctx,
11533 version,
11534 sketch_id,
11535 vec![ExistingSegmentCtor {
11536 id: line1_id,
11537 ctor: SegmentCtor::Line(LineCtor {
11538 start: point_expr_mm(2.0, 15.55),
11539 end: point_expr_mm(-4.03, 11.51),
11540 construction: None,
11541 }),
11542 }],
11543 EditSegmentsOptions {
11544 anchor_segment_ids: Some(vec![]),
11545 drag_anchors: vec![SegmentDragAnchor {
11546 segment_id: line1_id,
11547 target: label_position.clone(),
11548 }],
11549 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11550 constraint_id,
11551 label_position: label_position.clone(),
11552 }],
11553 commit_solved_initial_guesses: true,
11554 },
11555 )
11556 .await
11557 .unwrap();
11558
11559 assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11560 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11561 let ObjectKind::Constraint {
11562 constraint: Constraint::Distance(distance),
11563 } = &constraint_object.kind
11564 else {
11565 panic!("Expected distance constraint object");
11566 };
11567 assert_eq!(distance.label_position, Some(label_position));
11568
11569 let snapped_label_position = Point2d {
11570 x: Number {
11571 value: 8.0,
11572 units: NumericSuffix::Mm,
11573 },
11574 y: Number {
11575 value: 10.0,
11576 units: NumericSuffix::Mm,
11577 },
11578 };
11579 let (source_delta, scene_delta) = frontend
11580 .edit_segments_with_options(
11581 &mock_ctx,
11582 version,
11583 sketch_id,
11584 vec![],
11585 EditSegmentsOptions {
11586 anchor_segment_ids: Some(vec![line1_id]),
11587 drag_anchors: vec![],
11588 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11589 constraint_id,
11590 label_position: snapped_label_position.clone(),
11591 }],
11592 commit_solved_initial_guesses: true,
11593 },
11594 )
11595 .await
11596 .unwrap();
11597
11598 assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11599 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11600 let ObjectKind::Constraint {
11601 constraint: Constraint::Distance(distance),
11602 } = &constraint_object.kind
11603 else {
11604 panic!("Expected distance constraint object");
11605 };
11606 assert_eq!(distance.label_position, Some(snapped_label_position));
11607
11608 mock_ctx.close().await;
11609 }
11610
11611 #[tokio::test(flavor = "multi_thread")]
11612 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11613 let initial_source = "\
11614sketch(on = XY) {
11615 point1 = point(at = [var 0mm, var 0mm])
11616 point2 = point(at = [var 10mm, var 0mm])
11617 distance([point1, point2]) == 5mm
11618}
11619";
11620
11621 let program = Program::parse(initial_source).unwrap().0.unwrap();
11622 let mut frontend = FrontendState::new();
11623 let mock_ctx = ExecutorContext::new_mock(None).await;
11624 let version = Version(0);
11625
11626 frontend.program = program.clone();
11627 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11628 frontend.update_state_after_exec(outcome, true);
11629 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11630 let sketch_id = sketch_object.id;
11631 let sketch = expect_sketch(sketch_object);
11632 let point0_id = sketch.segments[0];
11633 let point1_id = sketch.segments[1];
11634 let constraint_id = sketch.constraints[0];
11635
11636 let edited_segments = vec![ExistingSegmentCtor {
11637 id: point0_id,
11638 ctor: SegmentCtor::Point(PointCtor {
11639 position: Point2d {
11640 x: Expr::Var(Number {
11641 value: 2.0,
11642 units: NumericSuffix::Mm,
11643 }),
11644 y: Expr::Var(Number {
11645 value: 1.0,
11646 units: NumericSuffix::Mm,
11647 }),
11648 },
11649 }),
11650 }];
11651 let (_, scene_delta) = frontend
11652 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11653 .await
11654 .unwrap();
11655 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11656 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11657
11658 let label_position = Point2d {
11659 x: Number {
11660 value: 3.0,
11661 units: NumericSuffix::Mm,
11662 },
11663 y: Number {
11664 value: 4.0,
11665 units: NumericSuffix::Mm,
11666 },
11667 };
11668 let (_, scene_delta) = frontend
11669 .edit_distance_constraint_label_position(
11670 &mock_ctx,
11671 version,
11672 sketch_id,
11673 constraint_id,
11674 label_position,
11675 vec![point0_id],
11676 )
11677 .await
11678 .unwrap();
11679
11680 assert_point_position_close(
11681 point_position(&scene_delta.new_graph, point0_id),
11682 point0_after_segment_edit,
11683 );
11684 assert_point_position_close(
11685 point_position(&scene_delta.new_graph, point1_id),
11686 point1_after_segment_edit,
11687 );
11688
11689 mock_ctx.close().await;
11690 }
11691
11692 #[tokio::test(flavor = "multi_thread")]
11693 async fn test_distance_point_line() {
11694 let initial_source = "\
11695sketch(on = XY) {
11696 point(at = [var 0, var 5])
11697 line(start = [var 0, var 0], end = [var 10, var 0])
11698}
11699";
11700
11701 let program = Program::parse(initial_source).unwrap().0.unwrap();
11702
11703 let mut frontend = FrontendState::new();
11704
11705 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11706 let mock_ctx = ExecutorContext::new_mock(None).await;
11707 let version = Version(0);
11708
11709 let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
11710 assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
11711 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11712 let sketch_id = sketch_object.id;
11713 let sketch = expect_sketch(sketch_object);
11714 let point_id = *sketch.segments.first().unwrap();
11715 let line_id = *sketch
11716 .segments
11717 .iter()
11718 .find(|segment_id| {
11719 matches!(
11720 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11721 Some(ObjectKind::Segment {
11722 segment: Segment::Line(_)
11723 })
11724 )
11725 })
11726 .unwrap();
11727
11728 let label_position = Point2d {
11729 x: Number {
11730 value: 10.0,
11731 units: NumericSuffix::Mm,
11732 },
11733 y: Number {
11734 value: 11.0,
11735 units: NumericSuffix::Mm,
11736 },
11737 };
11738 let constraint = Constraint::Distance(Distance {
11739 segments: vec![point_id.into(), line_id.into()],
11740 distance: Number {
11741 value: 5.0,
11742 units: NumericSuffix::Mm,
11743 },
11744 label_position: Some(label_position.clone()),
11745 source: Default::default(),
11746 });
11747 let (src_delta, scene_delta) = frontend
11748 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11749 .await
11750 .unwrap();
11751 insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11752 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11753 let sketch = expect_sketch(sketch_object);
11754 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11755 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11756 panic!("Expected constraint object");
11757 };
11758 let Constraint::Distance(distance) = constraint else {
11759 panic!("Expected distance constraint");
11760 };
11761 assert_eq!(distance.label_position, Some(label_position));
11762
11763 ctx.close().await;
11764 mock_ctx.close().await;
11765 }
11766
11767 #[tokio::test(flavor = "multi_thread")]
11768 async fn test_distance_point_arc() {
11769 let initial_source = "\
11770sketch(on = XY) {
11771 point(at = [var 0, var 8])
11772 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11773}
11774";
11775
11776 let program = Program::parse(initial_source).unwrap().0.unwrap();
11777
11778 let mut frontend = FrontendState::new();
11779
11780 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11781 let mock_ctx = ExecutorContext::new_mock(None).await;
11782 let version = Version(0);
11783
11784 frontend.hack_set_program(&ctx, program).await.unwrap();
11785 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11786 let sketch_id = sketch_object.id;
11787 let sketch = expect_sketch(sketch_object);
11788 let point_id = *sketch.segments.first().unwrap();
11789 let arc_id = *sketch
11790 .segments
11791 .iter()
11792 .find(|segment_id| {
11793 matches!(
11794 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11795 Some(ObjectKind::Segment {
11796 segment: Segment::Arc(_)
11797 })
11798 )
11799 })
11800 .unwrap();
11801
11802 let constraint = Constraint::Distance(Distance {
11803 segments: vec![point_id.into(), arc_id.into()],
11804 distance: Number {
11805 value: 3.0,
11806 units: NumericSuffix::Mm,
11807 },
11808 label_position: None,
11809 source: Default::default(),
11810 });
11811 let (src_delta, _scene_delta) = frontend
11812 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11813 .await
11814 .unwrap();
11815 insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11816
11817 ctx.close().await;
11818 mock_ctx.close().await;
11819 }
11820
11821 #[tokio::test(flavor = "multi_thread")]
11822 async fn test_distance_arc_origin() {
11823 let initial_source = "\
11824sketch001 = sketch(on = XY) {
11825 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11826}
11827";
11828
11829 let program = Program::parse(initial_source).unwrap().0.unwrap();
11830
11831 let mut frontend = FrontendState::new();
11832
11833 let mock_ctx = ExecutorContext::new_mock(None).await;
11834 let version = Version(0);
11835
11836 frontend.program = program.clone();
11837 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11838 frontend.update_state_after_exec(outcome, true);
11839 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11840 let sketch_id = sketch_object.id;
11841 let sketch = expect_sketch(sketch_object);
11842 let arc_id = *sketch
11843 .segments
11844 .iter()
11845 .find(|segment_id| {
11846 matches!(
11847 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11848 Some(ObjectKind::Segment {
11849 segment: Segment::Arc(_)
11850 })
11851 )
11852 })
11853 .unwrap();
11854
11855 let constraint = Constraint::Distance(Distance {
11856 segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11857 distance: Number {
11858 value: 3.0,
11859 units: NumericSuffix::Mm,
11860 },
11861 label_position: None,
11862 source: Default::default(),
11863 });
11864 let (src_delta, _scene_delta) = frontend
11865 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11866 .await
11867 .unwrap();
11868 insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11869
11870 mock_ctx.close().await;
11871 }
11872
11873 #[tokio::test(flavor = "multi_thread")]
11874 async fn test_distance_line_origin() {
11875 let initial_source = "\
11876sketch(on = XY) {
11877 line(start = [var 5, var 0], end = [var 5, var 10])
11878}
11879";
11880
11881 let program = Program::parse(initial_source).unwrap().0.unwrap();
11882
11883 let mut frontend = FrontendState::new();
11884
11885 let mock_ctx = ExecutorContext::new_mock(None).await;
11886 let version = Version(0);
11887
11888 frontend.program = program.clone();
11889 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11890 frontend.update_state_after_exec(outcome, true);
11891 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11892 let sketch_id = sketch_object.id;
11893 let sketch = expect_sketch(sketch_object);
11894 let line_id = *sketch
11895 .segments
11896 .iter()
11897 .find(|segment_id| {
11898 matches!(
11899 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11900 Some(ObjectKind::Segment {
11901 segment: Segment::Line(_)
11902 })
11903 )
11904 })
11905 .unwrap();
11906
11907 let constraint = Constraint::Distance(Distance {
11908 segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11909 distance: Number {
11910 value: 5.0,
11911 units: NumericSuffix::Mm,
11912 },
11913 label_position: None,
11914 source: Default::default(),
11915 });
11916 let (src_delta, _scene_delta) = frontend
11917 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11918 .await
11919 .unwrap();
11920 insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11921
11922 mock_ctx.close().await;
11923 }
11924
11925 #[tokio::test(flavor = "multi_thread")]
11926 async fn test_distance_line_circle() {
11927 let initial_source = "\
11928sketch(on = XY) {
11929 line(start = [var -10, var 8], end = [var 10, var 8])
11930 circle(start = [var 5, var 0], center = [var 0, var 0])
11931}
11932";
11933
11934 let program = Program::parse(initial_source).unwrap().0.unwrap();
11935
11936 let mut frontend = FrontendState::new();
11937
11938 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11939 let mock_ctx = ExecutorContext::new_mock(None).await;
11940 let version = Version(0);
11941
11942 frontend.hack_set_program(&ctx, program).await.unwrap();
11943 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11944 let sketch_id = sketch_object.id;
11945 let sketch = expect_sketch(sketch_object);
11946 let line_id = *sketch
11947 .segments
11948 .iter()
11949 .find(|segment_id| {
11950 matches!(
11951 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11952 Some(ObjectKind::Segment {
11953 segment: Segment::Line(_)
11954 })
11955 )
11956 })
11957 .unwrap();
11958 let circle_id = *sketch
11959 .segments
11960 .iter()
11961 .find(|segment_id| {
11962 matches!(
11963 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11964 Some(ObjectKind::Segment {
11965 segment: Segment::Circle(_)
11966 })
11967 )
11968 })
11969 .unwrap();
11970
11971 let constraint = Constraint::Distance(Distance {
11972 segments: vec![line_id.into(), circle_id.into()],
11973 distance: Number {
11974 value: 3.0,
11975 units: NumericSuffix::Mm,
11976 },
11977 label_position: None,
11978 source: Default::default(),
11979 });
11980 let (src_delta, _scene_delta) = frontend
11981 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11982 .await
11983 .unwrap();
11984 insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
11985
11986 ctx.close().await;
11987 mock_ctx.close().await;
11988 }
11989
11990 #[tokio::test(flavor = "multi_thread")]
11991 async fn test_distance_circle_arc() {
11992 let initial_source = "\
11993sketch(on = XY) {
11994 circle(start = [var 5, var 0], center = [var 0, var 0])
11995 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11996}
11997";
11998
11999 let program = Program::parse(initial_source).unwrap().0.unwrap();
12000
12001 let mut frontend = FrontendState::new();
12002
12003 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12004 let mock_ctx = ExecutorContext::new_mock(None).await;
12005 let version = Version(0);
12006
12007 let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
12008 assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
12009 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12010 let sketch_id = sketch_object.id;
12011 let sketch = expect_sketch(sketch_object);
12012 let circle_id = *sketch
12013 .segments
12014 .iter()
12015 .find(|segment_id| {
12016 matches!(
12017 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12018 Some(ObjectKind::Segment {
12019 segment: Segment::Circle(_)
12020 })
12021 )
12022 })
12023 .unwrap();
12024 let arc_id = *sketch
12025 .segments
12026 .iter()
12027 .find(|segment_id| {
12028 matches!(
12029 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12030 Some(ObjectKind::Segment {
12031 segment: Segment::Arc(_)
12032 })
12033 )
12034 })
12035 .unwrap();
12036
12037 let constraint = Constraint::Distance(Distance {
12038 segments: vec![circle_id.into(), arc_id.into()],
12039 distance: Number {
12040 value: 3.0,
12041 units: NumericSuffix::Mm,
12042 },
12043 label_position: None,
12044 source: Default::default(),
12045 });
12046 let (src_delta, _scene_delta) = frontend
12047 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12048 .await
12049 .unwrap();
12050 insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12051
12052 ctx.close().await;
12053 mock_ctx.close().await;
12054 }
12055
12056 #[tokio::test(flavor = "multi_thread")]
12057 async fn test_distance_parallel_lines() {
12058 let initial_source = "\
12059sketch(on = XY) {
12060 line(start = [var 0, var 0], end = [var 10, var 0])
12061 line(start = [var 0, var 5], end = [var 10, var 5])
12062}
12063";
12064
12065 let program = Program::parse(initial_source).unwrap().0.unwrap();
12066
12067 let mut frontend = FrontendState::new();
12068
12069 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12070 let mock_ctx = ExecutorContext::new_mock(None).await;
12071 let version = Version(0);
12072
12073 frontend.hack_set_program(&ctx, program).await.unwrap();
12074 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12075 let sketch_id = sketch_object.id;
12076 let sketch = expect_sketch(sketch_object);
12077 let line_ids = sketch
12078 .segments
12079 .iter()
12080 .copied()
12081 .filter(|segment_id| {
12082 matches!(
12083 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12084 Some(ObjectKind::Segment {
12085 segment: Segment::Line(_)
12086 })
12087 )
12088 })
12089 .collect::<Vec<_>>();
12090
12091 let constraint = Constraint::Distance(Distance {
12092 segments: vec![line_ids[0].into(), line_ids[1].into()],
12093 distance: Number {
12094 value: 5.0,
12095 units: NumericSuffix::Mm,
12096 },
12097 label_position: None,
12098 source: Default::default(),
12099 });
12100 let (src_delta, _scene_delta) = frontend
12101 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12102 .await
12103 .unwrap();
12104 insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12105
12106 ctx.close().await;
12107 mock_ctx.close().await;
12108 }
12109
12110 #[tokio::test(flavor = "multi_thread")]
12111 async fn test_distance_non_parallel_lines_lowers_to_distance() {
12112 let initial_source = "\
12117sketch(on = XY) {
12118 line(start = [var 0, var 0], end = [var 10, var 0])
12119 line(start = [var 0, var 0], end = [var 10, var 10])
12120}
12121";
12122
12123 let program = Program::parse(initial_source).unwrap().0.unwrap();
12124
12125 let mut frontend = FrontendState::new();
12126
12127 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12128 let mock_ctx = ExecutorContext::new_mock(None).await;
12129 let version = Version(0);
12130
12131 frontend.hack_set_program(&ctx, program).await.unwrap();
12132 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12133 let sketch_id = sketch_object.id;
12134 let sketch = expect_sketch(sketch_object);
12135 let line_ids = sketch
12136 .segments
12137 .iter()
12138 .copied()
12139 .filter(|segment_id| {
12140 matches!(
12141 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12142 Some(ObjectKind::Segment {
12143 segment: Segment::Line(_)
12144 })
12145 )
12146 })
12147 .collect::<Vec<_>>();
12148
12149 let constraint = Constraint::Distance(Distance {
12150 segments: vec![line_ids[0].into(), line_ids[1].into()],
12151 distance: Number {
12152 value: 5.0,
12153 units: NumericSuffix::Mm,
12154 },
12155 label_position: None,
12156 source: Default::default(),
12157 });
12158 let (src_delta, _scene_delta) = frontend
12159 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12160 .await
12161 .unwrap();
12162 insta::assert_snapshot!(
12163 "test_distance_non_parallel_lines_lowers_to_distance",
12164 src_delta.text.as_str()
12165 );
12166
12167 ctx.close().await;
12168 mock_ctx.close().await;
12169 }
12170
12171 #[tokio::test(flavor = "multi_thread")]
12172 async fn test_horizontal_distance_two_points() {
12173 let initial_source = "\
12174sketch(on = XY) {
12175 point(at = [var 1, var 2])
12176 point(at = [var 3, var 4])
12177}
12178";
12179
12180 let program = Program::parse(initial_source).unwrap().0.unwrap();
12181
12182 let mut frontend = FrontendState::new();
12183
12184 let mock_ctx = ExecutorContext::new_mock(None).await;
12185 let version = Version(0);
12186
12187 frontend.program = program.clone();
12188 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12189 frontend.update_state_after_exec(outcome, true);
12190 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12191 let sketch_id = sketch_object.id;
12192 let sketch = expect_sketch(sketch_object);
12193 let point0_id = *sketch.segments.first().unwrap();
12194 let point1_id = *sketch.segments.get(1).unwrap();
12195 let label_position = Point2d {
12196 x: Number {
12197 value: 10.0,
12198 units: NumericSuffix::Mm,
12199 },
12200 y: Number {
12201 value: 11.0,
12202 units: NumericSuffix::Mm,
12203 },
12204 };
12205
12206 let constraint = Constraint::HorizontalDistance(Distance {
12207 segments: vec![point0_id.into(), point1_id.into()],
12208 distance: Number {
12209 value: 2.0,
12210 units: NumericSuffix::Mm,
12211 },
12212 label_position: Some(label_position.clone()),
12213 source: Default::default(),
12214 });
12215 let (src_delta, scene_delta) = frontend
12216 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12217 .await
12218 .unwrap();
12219 insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12220 assert_eq!(
12221 scene_delta.new_graph.objects.len(),
12222 5,
12223 "{:#?}",
12224 scene_delta.new_graph.objects
12225 );
12226 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12227 let sketch = expect_sketch(sketch_object);
12228 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12229 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12230 panic!("Expected constraint object");
12231 };
12232 let Constraint::HorizontalDistance(distance) = constraint else {
12233 panic!("Expected horizontal distance constraint");
12234 };
12235 assert_eq!(distance.label_position, Some(label_position));
12236
12237 mock_ctx.close().await;
12238 }
12239
12240 #[tokio::test(flavor = "multi_thread")]
12241 async fn test_radius_single_arc_segment() {
12242 let initial_source = "\
12243sketch(on = XY) {
12244 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12245}
12246";
12247
12248 let program = Program::parse(initial_source).unwrap().0.unwrap();
12249
12250 let mut frontend = FrontendState::new();
12251
12252 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12253 let mock_ctx = ExecutorContext::new_mock(None).await;
12254 let version = Version(0);
12255
12256 frontend.hack_set_program(&ctx, program).await.unwrap();
12257 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12258 let sketch_id = sketch_object.id;
12259 let sketch = expect_sketch(sketch_object);
12260 let arc_id = sketch
12262 .segments
12263 .iter()
12264 .find(|&seg_id| {
12265 let obj = frontend.scene_graph.objects.get(seg_id.0);
12266 matches!(
12267 obj.map(|o| &o.kind),
12268 Some(ObjectKind::Segment {
12269 segment: Segment::Arc(_)
12270 })
12271 )
12272 })
12273 .unwrap();
12274
12275 let constraint = Constraint::Radius(Radius {
12276 arc: *arc_id,
12277 radius: Number {
12278 value: 5.0,
12279 units: NumericSuffix::Mm,
12280 },
12281 label_position: None,
12282 source: Default::default(),
12283 });
12284 let (src_delta, scene_delta) = frontend
12285 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12286 .await
12287 .unwrap();
12288 insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12289 assert_eq!(
12290 scene_delta.new_graph.objects.len(),
12291 7, "{:#?}",
12293 scene_delta.new_graph.objects
12294 );
12295
12296 ctx.close().await;
12297 mock_ctx.close().await;
12298 }
12299
12300 #[tokio::test(flavor = "multi_thread")]
12301 async fn test_radius_single_arc_segment_with_label_position() {
12302 let initial_source = "\
12303sketch(on = XY) {
12304 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12305}
12306";
12307
12308 let program = Program::parse(initial_source).unwrap().0.unwrap();
12309 let mut frontend = FrontendState::new();
12310 let mock_ctx = ExecutorContext::new_mock(None).await;
12311 let version = Version(0);
12312
12313 frontend.program = program.clone();
12314 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12315 frontend.update_state_after_exec(outcome, true);
12316 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12317 let sketch_id = sketch_object.id;
12318 let sketch = expect_sketch(sketch_object);
12319 let arc_id = sketch
12320 .segments
12321 .iter()
12322 .find(|&seg_id| {
12323 let obj = frontend.scene_graph.objects.get(seg_id.0);
12324 matches!(
12325 obj.map(|o| &o.kind),
12326 Some(ObjectKind::Segment {
12327 segment: Segment::Arc(_)
12328 })
12329 )
12330 })
12331 .unwrap();
12332
12333 let label_position = Point2d {
12334 x: Number {
12335 value: 10.0,
12336 units: NumericSuffix::Mm,
12337 },
12338 y: Number {
12339 value: 11.0,
12340 units: NumericSuffix::Mm,
12341 },
12342 };
12343 let constraint = Constraint::Radius(Radius {
12344 arc: *arc_id,
12345 radius: Number {
12346 value: 5.0,
12347 units: NumericSuffix::Mm,
12348 },
12349 label_position: Some(label_position.clone()),
12350 source: Default::default(),
12351 });
12352 let (src_delta, scene_delta) = frontend
12353 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12354 .await
12355 .unwrap();
12356 insta::assert_snapshot!(
12357 "test_radius_single_arc_segment_with_label_position",
12358 src_delta.text.as_str()
12359 );
12360
12361 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12362 let sketch = expect_sketch(sketch_object);
12363 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12364 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12365 panic!("Expected constraint object");
12366 };
12367 let Constraint::Radius(radius) = constraint else {
12368 panic!("Expected radius constraint");
12369 };
12370 assert_eq!(radius.label_position, Some(label_position));
12371
12372 mock_ctx.close().await;
12373 }
12374
12375 #[tokio::test(flavor = "multi_thread")]
12376 async fn test_edit_radius_constraint_label_position() {
12377 let initial_source = "\
12378sketch(on = XY) {
12379 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12380 radius(arc1) == 5mm
12381}
12382";
12383
12384 let program = Program::parse(initial_source).unwrap().0.unwrap();
12385 let mut frontend = FrontendState::new();
12386 let mock_ctx = ExecutorContext::new_mock(None).await;
12387 let version = Version(0);
12388
12389 frontend.program = program.clone();
12390 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12391 frontend.update_state_after_exec(outcome, true);
12392 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12393 let sketch_id = sketch_object.id;
12394 let sketch = expect_sketch(sketch_object);
12395 let constraint_id = sketch.constraints[0];
12396 let label_position = Point2d {
12397 x: Number {
12398 value: 10.0,
12399 units: NumericSuffix::Mm,
12400 },
12401 y: Number {
12402 value: 11.0,
12403 units: NumericSuffix::Mm,
12404 },
12405 };
12406
12407 let (src_delta, scene_delta) = frontend
12408 .edit_distance_constraint_label_position(
12409 &mock_ctx,
12410 version,
12411 sketch_id,
12412 constraint_id,
12413 label_position.clone(),
12414 vec![],
12415 )
12416 .await
12417 .unwrap();
12418 insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12419
12420 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12421 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12422 panic!("Expected constraint object");
12423 };
12424 let Constraint::Radius(radius) = constraint else {
12425 panic!("Expected radius constraint");
12426 };
12427 assert_eq!(radius.label_position, Some(label_position));
12428
12429 mock_ctx.close().await;
12430 }
12431
12432 #[tokio::test(flavor = "multi_thread")]
12433 async fn test_vertical_distance_two_points() {
12434 let initial_source = "\
12435sketch(on = XY) {
12436 point(at = [var 1, var 2])
12437 point(at = [var 3, var 4])
12438}
12439";
12440
12441 let program = Program::parse(initial_source).unwrap().0.unwrap();
12442
12443 let mut frontend = FrontendState::new();
12444
12445 let mock_ctx = ExecutorContext::new_mock(None).await;
12446 let version = Version(0);
12447
12448 frontend.program = program.clone();
12449 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12450 frontend.update_state_after_exec(outcome, true);
12451 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12452 let sketch_id = sketch_object.id;
12453 let sketch = expect_sketch(sketch_object);
12454 let point0_id = *sketch.segments.first().unwrap();
12455 let point1_id = *sketch.segments.get(1).unwrap();
12456 let label_position = Point2d {
12457 x: Number {
12458 value: 10.0,
12459 units: NumericSuffix::Mm,
12460 },
12461 y: Number {
12462 value: 11.0,
12463 units: NumericSuffix::Mm,
12464 },
12465 };
12466
12467 let constraint = Constraint::VerticalDistance(Distance {
12468 segments: vec![point0_id.into(), point1_id.into()],
12469 distance: Number {
12470 value: 2.0,
12471 units: NumericSuffix::Mm,
12472 },
12473 label_position: Some(label_position.clone()),
12474 source: Default::default(),
12475 });
12476 let (src_delta, scene_delta) = frontend
12477 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12478 .await
12479 .unwrap();
12480 insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12481 assert_eq!(
12482 scene_delta.new_graph.objects.len(),
12483 5,
12484 "{:#?}",
12485 scene_delta.new_graph.objects
12486 );
12487 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12488 let sketch = expect_sketch(sketch_object);
12489 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12490 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12491 panic!("Expected constraint object");
12492 };
12493 let Constraint::VerticalDistance(distance) = constraint else {
12494 panic!("Expected vertical distance constraint");
12495 };
12496 assert_eq!(distance.label_position, Some(label_position));
12497
12498 mock_ctx.close().await;
12499 }
12500
12501 #[tokio::test(flavor = "multi_thread")]
12502 async fn test_add_fixed_standalone_point() {
12503 let initial_source = "\
12504sketch(on = XY) {
12505 point(at = [var 1, var 2])
12506}
12507";
12508
12509 let program = Program::parse(initial_source).unwrap().0.unwrap();
12510
12511 let mut frontend = FrontendState::new();
12512
12513 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12514 let mock_ctx = ExecutorContext::new_mock(None).await;
12515 let version = Version(0);
12516
12517 frontend.hack_set_program(&ctx, program).await.unwrap();
12518 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12519 let sketch_id = sketch_object.id;
12520 let sketch = expect_sketch(sketch_object);
12521 let point_id = *sketch.segments.first().unwrap();
12522
12523 let (src_delta, scene_delta) = frontend
12524 .add_constraint(
12525 &mock_ctx,
12526 version,
12527 sketch_id,
12528 Constraint::Fixed(Fixed {
12529 points: vec![FixedPoint {
12530 point: point_id,
12531 position: Point2d {
12532 x: Number {
12533 value: 2.0,
12534 units: NumericSuffix::Mm,
12535 },
12536 y: Number {
12537 value: 3.0,
12538 units: NumericSuffix::Mm,
12539 },
12540 },
12541 }],
12542 }),
12543 )
12544 .await
12545 .unwrap();
12546 insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12547 assert_eq!(
12548 scene_delta.new_graph.objects.len(),
12549 4,
12550 "{:#?}",
12551 scene_delta.new_graph.objects
12552 );
12553
12554 ctx.close().await;
12555 mock_ctx.close().await;
12556 }
12557
12558 #[tokio::test(flavor = "multi_thread")]
12559 async fn test_add_fixed_multiple_points() {
12560 let initial_source = "\
12561sketch(on = XY) {
12562 point(at = [var 1, var 2])
12563 point(at = [var 3, var 4])
12564}
12565";
12566
12567 let program = Program::parse(initial_source).unwrap().0.unwrap();
12568
12569 let mut frontend = FrontendState::new();
12570
12571 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12572 let mock_ctx = ExecutorContext::new_mock(None).await;
12573 let version = Version(0);
12574
12575 frontend.hack_set_program(&ctx, program).await.unwrap();
12576 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12577 let sketch_id = sketch_object.id;
12578 let sketch = expect_sketch(sketch_object);
12579 let point0_id = *sketch.segments.first().unwrap();
12580 let point1_id = *sketch.segments.get(1).unwrap();
12581
12582 let (src_delta, scene_delta) = frontend
12583 .add_constraint(
12584 &mock_ctx,
12585 version,
12586 sketch_id,
12587 Constraint::Fixed(Fixed {
12588 points: vec![
12589 FixedPoint {
12590 point: point0_id,
12591 position: Point2d {
12592 x: Number {
12593 value: 2.0,
12594 units: NumericSuffix::Mm,
12595 },
12596 y: Number {
12597 value: 3.0,
12598 units: NumericSuffix::Mm,
12599 },
12600 },
12601 },
12602 FixedPoint {
12603 point: point1_id,
12604 position: Point2d {
12605 x: Number {
12606 value: 4.0,
12607 units: NumericSuffix::Mm,
12608 },
12609 y: Number {
12610 value: 5.0,
12611 units: NumericSuffix::Mm,
12612 },
12613 },
12614 },
12615 ],
12616 }),
12617 )
12618 .await
12619 .unwrap();
12620 insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12621 assert_eq!(
12622 scene_delta.new_graph.objects.len(),
12623 6,
12624 "{:#?}",
12625 scene_delta.new_graph.objects
12626 );
12627
12628 ctx.close().await;
12629 mock_ctx.close().await;
12630 }
12631
12632 #[tokio::test(flavor = "multi_thread")]
12633 async fn test_add_fixed_owned_point() {
12634 let initial_source = "\
12635sketch(on = XY) {
12636 line(start = [var 1, var 2], end = [var 3, var 4])
12637}
12638";
12639
12640 let program = Program::parse(initial_source).unwrap().0.unwrap();
12641
12642 let mut frontend = FrontendState::new();
12643
12644 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12645 let mock_ctx = ExecutorContext::new_mock(None).await;
12646 let version = Version(0);
12647
12648 frontend.hack_set_program(&ctx, program).await.unwrap();
12649 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12650 let sketch_id = sketch_object.id;
12651 let sketch = expect_sketch(sketch_object);
12652 let line_start_id = *sketch.segments.first().unwrap();
12653
12654 let (src_delta, scene_delta) = frontend
12655 .add_constraint(
12656 &mock_ctx,
12657 version,
12658 sketch_id,
12659 Constraint::Fixed(Fixed {
12660 points: vec![FixedPoint {
12661 point: line_start_id,
12662 position: Point2d {
12663 x: Number {
12664 value: 2.0,
12665 units: NumericSuffix::Mm,
12666 },
12667 y: Number {
12668 value: 3.0,
12669 units: NumericSuffix::Mm,
12670 },
12671 },
12672 }],
12673 }),
12674 )
12675 .await
12676 .unwrap();
12677 insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12678 assert_eq!(
12679 scene_delta.new_graph.objects.len(),
12680 6,
12681 "{:#?}",
12682 scene_delta.new_graph.objects
12683 );
12684
12685 ctx.close().await;
12686 mock_ctx.close().await;
12687 }
12688
12689 #[tokio::test(flavor = "multi_thread")]
12690 async fn test_radius_error_cases() {
12691 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12692 let mock_ctx = ExecutorContext::new_mock(None).await;
12693 let version = Version(0);
12694
12695 let initial_source_point = "\
12697sketch(on = XY) {
12698 point(at = [var 1, var 2])
12699}
12700";
12701 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12702 let mut frontend_point = FrontendState::new();
12703 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12704 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12705 let sketch_id_point = sketch_object_point.id;
12706 let sketch_point = expect_sketch(sketch_object_point);
12707 let point_id = *sketch_point.segments.first().unwrap();
12708
12709 let constraint_point = Constraint::Radius(Radius {
12710 arc: point_id,
12711 radius: Number {
12712 value: 5.0,
12713 units: NumericSuffix::Mm,
12714 },
12715 label_position: None,
12716 source: Default::default(),
12717 });
12718 let result_point = frontend_point
12719 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12720 .await;
12721 assert!(result_point.is_err(), "Single point should error for radius");
12722
12723 let initial_source_line = "\
12725sketch(on = XY) {
12726 line(start = [var 1, var 2], end = [var 3, var 4])
12727}
12728";
12729 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12730 let mut frontend_line = FrontendState::new();
12731 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12732 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12733 let sketch_id_line = sketch_object_line.id;
12734 let sketch_line = expect_sketch(sketch_object_line);
12735 let line_id = *sketch_line.segments.first().unwrap();
12736
12737 let constraint_line = Constraint::Radius(Radius {
12738 arc: line_id,
12739 radius: Number {
12740 value: 5.0,
12741 units: NumericSuffix::Mm,
12742 },
12743 label_position: None,
12744 source: Default::default(),
12745 });
12746 let result_line = frontend_line
12747 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12748 .await;
12749 assert!(result_line.is_err(), "Single line segment should error for radius");
12750
12751 ctx.close().await;
12752 mock_ctx.close().await;
12753 }
12754
12755 #[tokio::test(flavor = "multi_thread")]
12756 async fn test_diameter_single_arc_segment() {
12757 let initial_source = "\
12758sketch(on = XY) {
12759 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12760}
12761";
12762
12763 let program = Program::parse(initial_source).unwrap().0.unwrap();
12764
12765 let mut frontend = FrontendState::new();
12766
12767 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12768 let mock_ctx = ExecutorContext::new_mock(None).await;
12769 let version = Version(0);
12770
12771 frontend.hack_set_program(&ctx, program).await.unwrap();
12772 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12773 let sketch_id = sketch_object.id;
12774 let sketch = expect_sketch(sketch_object);
12775 let arc_id = sketch
12777 .segments
12778 .iter()
12779 .find(|&seg_id| {
12780 let obj = frontend.scene_graph.objects.get(seg_id.0);
12781 matches!(
12782 obj.map(|o| &o.kind),
12783 Some(ObjectKind::Segment {
12784 segment: Segment::Arc(_)
12785 })
12786 )
12787 })
12788 .unwrap();
12789
12790 let constraint = Constraint::Diameter(Diameter {
12791 arc: *arc_id,
12792 diameter: Number {
12793 value: 10.0,
12794 units: NumericSuffix::Mm,
12795 },
12796 label_position: None,
12797 source: Default::default(),
12798 });
12799 let (src_delta, scene_delta) = frontend
12800 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12801 .await
12802 .unwrap();
12803 insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12804 assert_eq!(
12805 scene_delta.new_graph.objects.len(),
12806 7, "{:#?}",
12808 scene_delta.new_graph.objects
12809 );
12810
12811 ctx.close().await;
12812 mock_ctx.close().await;
12813 }
12814
12815 #[tokio::test(flavor = "multi_thread")]
12816 async fn test_diameter_single_arc_segment_with_label_position() {
12817 let initial_source = "\
12818sketch(on = XY) {
12819 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12820}
12821";
12822
12823 let program = Program::parse(initial_source).unwrap().0.unwrap();
12824 let mut frontend = FrontendState::new();
12825 let mock_ctx = ExecutorContext::new_mock(None).await;
12826 let version = Version(0);
12827
12828 frontend.program = program.clone();
12829 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12830 frontend.update_state_after_exec(outcome, true);
12831 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12832 let sketch_id = sketch_object.id;
12833 let sketch = expect_sketch(sketch_object);
12834 let arc_id = sketch
12835 .segments
12836 .iter()
12837 .find(|&seg_id| {
12838 let obj = frontend.scene_graph.objects.get(seg_id.0);
12839 matches!(
12840 obj.map(|o| &o.kind),
12841 Some(ObjectKind::Segment {
12842 segment: Segment::Arc(_)
12843 })
12844 )
12845 })
12846 .unwrap();
12847
12848 let label_position = Point2d {
12849 x: Number {
12850 value: 10.0,
12851 units: NumericSuffix::Mm,
12852 },
12853 y: Number {
12854 value: 11.0,
12855 units: NumericSuffix::Mm,
12856 },
12857 };
12858 let constraint = Constraint::Diameter(Diameter {
12859 arc: *arc_id,
12860 diameter: Number {
12861 value: 10.0,
12862 units: NumericSuffix::Mm,
12863 },
12864 label_position: Some(label_position.clone()),
12865 source: Default::default(),
12866 });
12867 let (src_delta, scene_delta) = frontend
12868 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12869 .await
12870 .unwrap();
12871 insta::assert_snapshot!(
12872 "test_diameter_single_arc_segment_with_label_position",
12873 src_delta.text.as_str()
12874 );
12875
12876 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12877 let sketch = expect_sketch(sketch_object);
12878 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12879 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12880 panic!("Expected constraint object");
12881 };
12882 let Constraint::Diameter(diameter) = constraint else {
12883 panic!("Expected diameter constraint");
12884 };
12885 assert_eq!(diameter.label_position, Some(label_position));
12886
12887 mock_ctx.close().await;
12888 }
12889
12890 #[tokio::test(flavor = "multi_thread")]
12891 async fn test_edit_diameter_constraint_label_position() {
12892 let initial_source = "\
12893sketch(on = XY) {
12894 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12895 diameter(arc1) == 10mm
12896}
12897";
12898
12899 let program = Program::parse(initial_source).unwrap().0.unwrap();
12900 let mut frontend = FrontendState::new();
12901 let mock_ctx = ExecutorContext::new_mock(None).await;
12902 let version = Version(0);
12903
12904 frontend.program = program.clone();
12905 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12906 frontend.update_state_after_exec(outcome, true);
12907 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12908 let sketch_id = sketch_object.id;
12909 let sketch = expect_sketch(sketch_object);
12910 let constraint_id = sketch.constraints[0];
12911 let label_position = Point2d {
12912 x: Number {
12913 value: 10.0,
12914 units: NumericSuffix::Mm,
12915 },
12916 y: Number {
12917 value: 11.0,
12918 units: NumericSuffix::Mm,
12919 },
12920 };
12921
12922 let (src_delta, scene_delta) = frontend
12923 .edit_distance_constraint_label_position(
12924 &mock_ctx,
12925 version,
12926 sketch_id,
12927 constraint_id,
12928 label_position.clone(),
12929 vec![],
12930 )
12931 .await
12932 .unwrap();
12933 insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12934
12935 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12936 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12937 panic!("Expected constraint object");
12938 };
12939 let Constraint::Diameter(diameter) = constraint else {
12940 panic!("Expected diameter constraint");
12941 };
12942 assert_eq!(diameter.label_position, Some(label_position));
12943
12944 mock_ctx.close().await;
12945 }
12946
12947 #[tokio::test(flavor = "multi_thread")]
12948 async fn test_diameter_error_cases() {
12949 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12950 let mock_ctx = ExecutorContext::new_mock(None).await;
12951 let version = Version(0);
12952
12953 let initial_source_point = "\
12955sketch(on = XY) {
12956 point(at = [var 1, var 2])
12957}
12958";
12959 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12960 let mut frontend_point = FrontendState::new();
12961 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12962 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12963 let sketch_id_point = sketch_object_point.id;
12964 let sketch_point = expect_sketch(sketch_object_point);
12965 let point_id = *sketch_point.segments.first().unwrap();
12966
12967 let constraint_point = Constraint::Diameter(Diameter {
12968 arc: point_id,
12969 diameter: Number {
12970 value: 10.0,
12971 units: NumericSuffix::Mm,
12972 },
12973 label_position: None,
12974 source: Default::default(),
12975 });
12976 let result_point = frontend_point
12977 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12978 .await;
12979 assert!(result_point.is_err(), "Single point should error for diameter");
12980
12981 let initial_source_line = "\
12983sketch(on = XY) {
12984 line(start = [var 1, var 2], end = [var 3, var 4])
12985}
12986";
12987 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12988 let mut frontend_line = FrontendState::new();
12989 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12990 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12991 let sketch_id_line = sketch_object_line.id;
12992 let sketch_line = expect_sketch(sketch_object_line);
12993 let line_id = *sketch_line.segments.first().unwrap();
12994
12995 let constraint_line = Constraint::Diameter(Diameter {
12996 arc: line_id,
12997 diameter: Number {
12998 value: 10.0,
12999 units: NumericSuffix::Mm,
13000 },
13001 label_position: None,
13002 source: Default::default(),
13003 });
13004 let result_line = frontend_line
13005 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
13006 .await;
13007 assert!(result_line.is_err(), "Single line segment should error for diameter");
13008
13009 ctx.close().await;
13010 mock_ctx.close().await;
13011 }
13012
13013 #[tokio::test(flavor = "multi_thread")]
13014 async fn test_line_horizontal() {
13015 let initial_source = "\
13016sketch(on = XY) {
13017 line(start = [var 1, var 2], end = [var 3, var 4])
13018}
13019";
13020
13021 let program = Program::parse(initial_source).unwrap().0.unwrap();
13022
13023 let mut frontend = FrontendState::new();
13024
13025 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13026 let mock_ctx = ExecutorContext::new_mock(None).await;
13027 let version = Version(0);
13028
13029 frontend.hack_set_program(&ctx, program).await.unwrap();
13030 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13031 let sketch_id = sketch_object.id;
13032 let sketch = expect_sketch(sketch_object);
13033 let line1_id = *sketch.segments.get(2).unwrap();
13034
13035 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13036 let (src_delta, scene_delta) = frontend
13037 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13038 .await
13039 .unwrap();
13040 insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13041 assert_eq!(
13042 scene_delta.new_graph.objects.len(),
13043 6,
13044 "{:#?}",
13045 scene_delta.new_graph.objects
13046 );
13047
13048 ctx.close().await;
13049 mock_ctx.close().await;
13050 }
13051
13052 #[tokio::test(flavor = "multi_thread")]
13053 async fn test_control_point_spline_edge_horizontal() {
13054 let initial_source = "\
13055@settings(experimentalFeatures = allow)
13056splineSketch = sketch(on = XY) {
13057 controlPointSpline1 = controlPointSpline(points = [
13058 [var 0mm, var 0mm],
13059 [var 10mm, var 20mm],
13060 [var 20mm, var 0mm],
13061 ])
13062}
13063";
13064
13065 let program = Program::parse(initial_source).unwrap().0.unwrap();
13066
13067 let mut frontend = FrontendState::new();
13068
13069 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13070 let mock_ctx = ExecutorContext::new_mock(None).await;
13071 let version = Version(0);
13072
13073 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13074 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13075 let sketch_id = sketch_object.id;
13076 let sketch = expect_sketch(sketch_object);
13077 let spline_id = sketch
13078 .segments
13079 .iter()
13080 .copied()
13081 .find(|seg_id| {
13082 matches!(
13083 &frontend.scene_graph.objects[seg_id.0].kind,
13084 ObjectKind::Segment {
13085 segment: Segment::ControlPointSpline(_)
13086 }
13087 )
13088 })
13089 .expect("Expected a control point spline segment in sketch");
13090 let edge_id = frontend
13091 .scene_graph
13092 .objects
13093 .iter()
13094 .find_map(|obj| match &obj.kind {
13095 ObjectKind::Segment {
13096 segment: Segment::Line(line),
13097 } if line.owner == Some(spline_id) => Some(obj.id),
13098 _ => None,
13099 })
13100 .expect("Expected an owned control-polygon edge");
13101
13102 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13103 let (src_delta, _) = frontend
13104 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13105 .await
13106 .unwrap();
13107 assert!(
13108 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13109 "Expected horizontal constraint on spline edge, got: {}",
13110 src_delta.text
13111 );
13112
13113 ctx.close().await;
13114 mock_ctx.close().await;
13115 }
13116
13117 #[tokio::test(flavor = "multi_thread")]
13118 async fn test_control_point_spline_edge_angle() {
13119 let initial_source = "\
13120@settings(experimentalFeatures = allow)
13121splineSketch = sketch(on = XY) {
13122 controlPointSpline1 = controlPointSpline(points = [
13123 [var 0mm, var 0mm],
13124 [var 10mm, var 20mm],
13125 [var 20mm, var 0mm],
13126 ])
13127
13128 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13129}
13130";
13131
13132 let program = Program::parse(initial_source).unwrap().0.unwrap();
13133
13134 let mut frontend = FrontendState::new();
13135
13136 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13137 let mock_ctx = ExecutorContext::new_mock(None).await;
13138 let version = Version(0);
13139
13140 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13141 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13142 let sketch_id = sketch_object.id;
13143 let sketch = expect_sketch(sketch_object);
13144 let spline_id = sketch
13145 .segments
13146 .iter()
13147 .copied()
13148 .find(|seg_id| {
13149 matches!(
13150 &frontend.scene_graph.objects[seg_id.0].kind,
13151 ObjectKind::Segment {
13152 segment: Segment::ControlPointSpline(_)
13153 }
13154 )
13155 })
13156 .expect("Expected a control point spline segment in sketch");
13157 let edge_id = frontend
13158 .scene_graph
13159 .objects
13160 .iter()
13161 .find_map(|obj| match &obj.kind {
13162 ObjectKind::Segment {
13163 segment: Segment::Line(line),
13164 } if line.owner == Some(spline_id) => Some(obj.id),
13165 _ => None,
13166 })
13167 .expect("Expected an owned control-polygon edge");
13168 let line1_id = frontend
13169 .scene_graph
13170 .objects
13171 .iter()
13172 .find_map(|obj| match &obj.kind {
13173 ObjectKind::Segment {
13174 segment: Segment::Line(line),
13175 } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
13176 _ => None,
13177 })
13178 .or_else(|| {
13179 sketch.segments.iter().copied().find(|seg_id| {
13180 matches!(
13181 &frontend.scene_graph.objects[seg_id.0].kind,
13182 ObjectKind::Segment {
13183 segment: Segment::Line(line),
13184 } if line.owner.is_none()
13185 )
13186 })
13187 })
13188 .expect("Expected a standalone line segment in sketch");
13189
13190 let constraint = Constraint::Angle(Angle {
13191 lines: vec![line1_id, edge_id],
13192 angle: Number {
13193 value: 30.0,
13194 units: NumericSuffix::Deg,
13195 },
13196 sector: None,
13197 inverse: None,
13198 label_position: None,
13199 source: Default::default(),
13200 });
13201 let (src_delta, _) = frontend
13202 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13203 .await
13204 .unwrap();
13205 assert!(
13206 src_delta
13207 .text
13208 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13209 "Expected angle constraint on spline edge, got: {}",
13210 src_delta.text
13211 );
13212
13213 ctx.close().await;
13214 mock_ctx.close().await;
13215 }
13216
13217 #[tokio::test(flavor = "multi_thread")]
13218 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13219 let initial_source = "\
13220@settings(experimentalFeatures = allow)
13221splineSketch = sketch(on = XY) {
13222 spline1 = controlPointSpline(points = [
13223 [var 0mm, var 0mm],
13224 [var 10mm, var 20mm],
13225 [var 20mm, var 0mm],
13226 ])
13227 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13228 coincident([spline1.controls[1], spline1.edges[0]])
13229 coincident([spline1.controls[0], line1])
13230}
13231";
13232
13233 let program = Program::parse(initial_source).unwrap().0.unwrap();
13234
13235 let mut frontend = FrontendState::new();
13236
13237 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13238 let mock_ctx = ExecutorContext::new_mock(None).await;
13239
13240 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13241
13242 let ui_scene_graph = frontend.scene_graph_for_ui();
13243 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13244 let sketch = expect_sketch(sketch_object);
13245
13246 assert_eq!(
13247 sketch.constraints.len(),
13248 1,
13249 "Expected only the external coincident constraint to remain visible in the UI scene graph"
13250 );
13251
13252 let visible_constraints = ui_scene_graph
13253 .objects
13254 .iter()
13255 .filter_map(|object| match &object.kind {
13256 ObjectKind::Constraint {
13257 constraint: Constraint::Coincident(coincident),
13258 } => Some(coincident.clone()),
13259 _ => None,
13260 })
13261 .collect::<Vec<_>>();
13262
13263 assert_eq!(
13264 visible_constraints.len(),
13265 1,
13266 "Expected only one coincident constraint object in the UI scene graph"
13267 );
13268 assert_eq!(
13269 visible_constraints[0].get_segments().len(),
13270 2,
13271 "Expected the remaining visible coincident constraint to reference two segments"
13272 );
13273
13274 ctx.close().await;
13275 mock_ctx.close().await;
13276 }
13277
13278 #[tokio::test(flavor = "multi_thread")]
13279 async fn test_edit_control_point_spline_can_append_control_point() {
13280 let initial_source = "\
13281@settings(experimentalFeatures = allow)
13282splineSketch = sketch(on = XY) {
13283 controlPointSpline(points = [
13284 [var 0mm, var 0mm],
13285 [var 10mm, var 20mm],
13286 [var 20mm, var 0mm],
13287 ])
13288}
13289";
13290
13291 let program = Program::parse(initial_source).unwrap().0.unwrap();
13292
13293 let mut frontend = FrontendState::new();
13294
13295 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13296 let mock_ctx = ExecutorContext::new_mock(None).await;
13297 let version = Version(0);
13298
13299 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13300 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13301 let sketch_id = sketch_object.id;
13302 let sketch = expect_sketch(sketch_object);
13303 let spline_id = sketch
13304 .segments
13305 .iter()
13306 .copied()
13307 .find(|seg_id| {
13308 matches!(
13309 &frontend.scene_graph.objects[seg_id.0].kind,
13310 ObjectKind::Segment {
13311 segment: Segment::ControlPointSpline(_)
13312 }
13313 )
13314 })
13315 .expect("Expected a control point spline segment in sketch");
13316
13317 let ctor = ControlPointSplineCtor {
13318 points: vec![
13319 Point2d {
13320 x: Expr::Var(Number {
13321 value: 0.0,
13322 units: NumericSuffix::Mm,
13323 }),
13324 y: Expr::Var(Number {
13325 value: 0.0,
13326 units: NumericSuffix::Mm,
13327 }),
13328 },
13329 Point2d {
13330 x: Expr::Var(Number {
13331 value: 10.0,
13332 units: NumericSuffix::Mm,
13333 }),
13334 y: Expr::Var(Number {
13335 value: 20.0,
13336 units: NumericSuffix::Mm,
13337 }),
13338 },
13339 Point2d {
13340 x: Expr::Var(Number {
13341 value: 20.0,
13342 units: NumericSuffix::Mm,
13343 }),
13344 y: Expr::Var(Number {
13345 value: 0.0,
13346 units: NumericSuffix::Mm,
13347 }),
13348 },
13349 Point2d {
13350 x: Expr::Var(Number {
13351 value: 30.0,
13352 units: NumericSuffix::Mm,
13353 }),
13354 y: Expr::Var(Number {
13355 value: 10.0,
13356 units: NumericSuffix::Mm,
13357 }),
13358 },
13359 ],
13360 construction: None,
13361 };
13362
13363 let segments = vec![ExistingSegmentCtor {
13364 id: spline_id,
13365 ctor: SegmentCtor::ControlPointSpline(ctor),
13366 }];
13367 let (src_delta, scene_delta) = frontend
13368 .edit_segments(&mock_ctx, version, sketch_id, segments)
13369 .await
13370 .unwrap();
13371
13372 assert!(
13373 src_delta.text.contains("[var 30mm, var 10mm]"),
13374 "Expected appended spline control point in source, got: {}",
13375 src_delta.text
13376 );
13377
13378 assert!(
13379 scene_delta.invalidates_ids,
13380 "Expected appending a spline control point to invalidate ids"
13381 );
13382 let updated_spline = scene_delta
13383 .new_graph
13384 .objects
13385 .iter()
13386 .find_map(|obj| match &obj.kind {
13387 ObjectKind::Segment {
13388 segment: Segment::ControlPointSpline(updated_spline),
13389 } if updated_spline.controls.len() == 4 => Some(updated_spline),
13390 _ => None,
13391 })
13392 .expect("Expected edited scene graph to contain a four-point control point spline");
13393 assert_eq!(
13394 updated_spline.controls.len(),
13395 4,
13396 "Expected edited spline to expose four control points"
13397 );
13398
13399 ctx.close().await;
13400 mock_ctx.close().await;
13401 }
13402
13403 #[tokio::test(flavor = "multi_thread")]
13404 async fn test_line_vertical() {
13405 let initial_source = "\
13406sketch(on = XY) {
13407 line(start = [var 1, var 2], end = [var 3, var 4])
13408}
13409";
13410
13411 let program = Program::parse(initial_source).unwrap().0.unwrap();
13412
13413 let mut frontend = FrontendState::new();
13414
13415 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13416 let mock_ctx = ExecutorContext::new_mock(None).await;
13417 let version = Version(0);
13418
13419 frontend.hack_set_program(&ctx, program).await.unwrap();
13420 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13421 let sketch_id = sketch_object.id;
13422 let sketch = expect_sketch(sketch_object);
13423 let line1_id = *sketch.segments.get(2).unwrap();
13424
13425 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13426 let (src_delta, scene_delta) = frontend
13427 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13428 .await
13429 .unwrap();
13430 insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13431 assert_eq!(
13432 scene_delta.new_graph.objects.len(),
13433 6,
13434 "{:#?}",
13435 scene_delta.new_graph.objects
13436 );
13437
13438 ctx.close().await;
13439 mock_ctx.close().await;
13440 }
13441
13442 #[tokio::test(flavor = "multi_thread")]
13443 async fn test_points_vertical() {
13444 let initial_source = "\
13445sketch001 = sketch(on = XY) {
13446 p0 = point(at = [var -2.23mm, var 3.1mm])
13447 pf = point(at = [4, 4])
13448}
13449";
13450
13451 let program = Program::parse(initial_source).unwrap().0.unwrap();
13452
13453 let mut frontend = FrontendState::new();
13454
13455 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13456 let mock_ctx = ExecutorContext::new_mock(None).await;
13457 let version = Version(0);
13458
13459 frontend.hack_set_program(&ctx, program).await.unwrap();
13460 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13461 let sketch_id = sketch_object.id;
13462 let sketch = expect_sketch(sketch_object);
13463 let point_ids = vec![
13464 sketch.segments.first().unwrap().to_owned(),
13465 sketch.segments.get(1).unwrap().to_owned(),
13466 ];
13467
13468 let constraint = Constraint::Vertical(Vertical::Points {
13469 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13470 });
13471 let (src_delta, scene_delta) = frontend
13472 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13473 .await
13474 .unwrap();
13475 insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13476 assert_eq!(
13477 scene_delta.new_graph.objects.len(),
13478 5,
13479 "{:#?}",
13480 scene_delta.new_graph.objects
13481 );
13482
13483 ctx.close().await;
13484 mock_ctx.close().await;
13485 }
13486
13487 #[tokio::test(flavor = "multi_thread")]
13488 async fn test_points_horizontal() {
13489 let initial_source = "\
13490sketch001 = sketch(on = XY) {
13491 p0 = point(at = [var -2.23mm, var 3.1mm])
13492 pf = point(at = [4, 4])
13493}
13494";
13495
13496 let program = Program::parse(initial_source).unwrap().0.unwrap();
13497
13498 let mut frontend = FrontendState::new();
13499
13500 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13501 let mock_ctx = ExecutorContext::new_mock(None).await;
13502 let version = Version(0);
13503
13504 frontend.hack_set_program(&ctx, program).await.unwrap();
13505 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13506 let sketch_id = sketch_object.id;
13507 let sketch = expect_sketch(sketch_object);
13508 let point_ids = vec![
13509 sketch.segments.first().unwrap().to_owned(),
13510 sketch.segments.get(1).unwrap().to_owned(),
13511 ];
13512
13513 let constraint = Constraint::Horizontal(Horizontal::Points {
13514 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13515 });
13516 let (src_delta, scene_delta) = frontend
13517 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13518 .await
13519 .unwrap();
13520 insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13521 assert_eq!(
13522 scene_delta.new_graph.objects.len(),
13523 5,
13524 "{:#?}",
13525 scene_delta.new_graph.objects
13526 );
13527
13528 ctx.close().await;
13529 mock_ctx.close().await;
13530 }
13531
13532 #[tokio::test(flavor = "multi_thread")]
13533 async fn test_point_horizontal_with_origin() {
13534 let initial_source = "\
13535sketch001 = sketch(on = XY) {
13536 p0 = point(at = [var -2.23mm, var 3.1mm])
13537}
13538";
13539
13540 let program = Program::parse(initial_source).unwrap().0.unwrap();
13541
13542 let mut frontend = FrontendState::new();
13543
13544 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13545 let mock_ctx = ExecutorContext::new_mock(None).await;
13546 let version = Version(0);
13547
13548 frontend.hack_set_program(&ctx, program).await.unwrap();
13549 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13550 let sketch_id = sketch_object.id;
13551 let sketch = expect_sketch(sketch_object);
13552 let point_id = *sketch.segments.first().unwrap();
13553
13554 let constraint = Constraint::Horizontal(Horizontal::Points {
13555 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13556 });
13557 let (src_delta, scene_delta) = frontend
13558 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13559 .await
13560 .unwrap();
13561 insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13562 assert_eq!(
13563 scene_delta.new_graph.objects.len(),
13564 4,
13565 "{:#?}",
13566 scene_delta.new_graph.objects
13567 );
13568
13569 ctx.close().await;
13570 mock_ctx.close().await;
13571 }
13572
13573 #[tokio::test(flavor = "multi_thread")]
13574 async fn test_lines_equal_length() {
13575 let initial_source = "\
13576sketch(on = XY) {
13577 line(start = [var 1, var 2], end = [var 3, var 4])
13578 line(start = [var 5, var 6], end = [var 7, var 8])
13579}
13580";
13581
13582 let program = Program::parse(initial_source).unwrap().0.unwrap();
13583
13584 let mut frontend = FrontendState::new();
13585
13586 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13587 let mock_ctx = ExecutorContext::new_mock(None).await;
13588 let version = Version(0);
13589
13590 frontend.hack_set_program(&ctx, program).await.unwrap();
13591 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13592 let sketch_id = sketch_object.id;
13593 let sketch = expect_sketch(sketch_object);
13594 let line1_id = *sketch.segments.get(2).unwrap();
13595 let line2_id = *sketch.segments.get(5).unwrap();
13596
13597 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13598 lines: vec![line1_id, line2_id],
13599 });
13600 let (src_delta, scene_delta) = frontend
13601 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13602 .await
13603 .unwrap();
13604 insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13605 assert_eq!(
13606 scene_delta.new_graph.objects.len(),
13607 9,
13608 "{:#?}",
13609 scene_delta.new_graph.objects
13610 );
13611
13612 ctx.close().await;
13613 mock_ctx.close().await;
13614 }
13615
13616 #[tokio::test(flavor = "multi_thread")]
13617 async fn test_add_constraint_multi_line_equal_length() {
13618 let initial_source = "\
13619sketch(on = XY) {
13620 line(start = [var 1, var 2], end = [var 3, var 4])
13621 line(start = [var 5, var 6], end = [var 7, var 8])
13622 line(start = [var 9, var 10], end = [var 11, var 12])
13623}
13624";
13625
13626 let program = Program::parse(initial_source).unwrap().0.unwrap();
13627
13628 let mut frontend = FrontendState::new();
13629 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13630 let mock_ctx = ExecutorContext::new_mock(None).await;
13631 let version = Version(0);
13632
13633 frontend.hack_set_program(&ctx, program).await.unwrap();
13634 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13635 let sketch_id = sketch_object.id;
13636 let sketch = expect_sketch(sketch_object);
13637 let line1_id = *sketch.segments.get(2).unwrap();
13638 let line2_id = *sketch.segments.get(5).unwrap();
13639 let line3_id = *sketch.segments.get(8).unwrap();
13640
13641 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13642 lines: vec![line1_id, line2_id, line3_id],
13643 });
13644 let (src_delta, scene_delta) = frontend
13645 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13646 .await
13647 .unwrap();
13648 insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13649 let constraints = scene_delta
13650 .new_graph
13651 .objects
13652 .iter()
13653 .filter_map(|obj| {
13654 let ObjectKind::Constraint { constraint } = &obj.kind else {
13655 return None;
13656 };
13657 Some(constraint)
13658 })
13659 .collect::<Vec<_>>();
13660
13661 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13662 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13663 panic!("expected equal length constraint, got {:?}", constraints[0]);
13664 };
13665 assert_eq!(lines_equal_length.lines.len(), 3);
13666
13667 ctx.close().await;
13668 mock_ctx.close().await;
13669 }
13670
13671 #[tokio::test(flavor = "multi_thread")]
13672 async fn test_lines_parallel() {
13673 let initial_source = "\
13674sketch(on = XY) {
13675 line(start = [var 1, var 2], end = [var 3, var 4])
13676 line(start = [var 5, var 6], end = [var 7, var 8])
13677}
13678";
13679
13680 let program = Program::parse(initial_source).unwrap().0.unwrap();
13681
13682 let mut frontend = FrontendState::new();
13683
13684 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13685 let mock_ctx = ExecutorContext::new_mock(None).await;
13686 let version = Version(0);
13687
13688 frontend.hack_set_program(&ctx, program).await.unwrap();
13689 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13690 let sketch_id = sketch_object.id;
13691 let sketch = expect_sketch(sketch_object);
13692 let line1_id = *sketch.segments.get(2).unwrap();
13693 let line2_id = *sketch.segments.get(5).unwrap();
13694
13695 let constraint = Constraint::Parallel(Parallel {
13696 lines: vec![line1_id, line2_id],
13697 });
13698 let (src_delta, scene_delta) = frontend
13699 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13700 .await
13701 .unwrap();
13702 insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13703 assert_eq!(
13704 scene_delta.new_graph.objects.len(),
13705 9,
13706 "{:#?}",
13707 scene_delta.new_graph.objects
13708 );
13709
13710 ctx.close().await;
13711 mock_ctx.close().await;
13712 }
13713
13714 #[tokio::test(flavor = "multi_thread")]
13715 async fn test_lines_parallel_multiline() {
13716 let initial_source = "\
13717sketch(on = XY) {
13718 line(start = [var 1, var 2], end = [var 3, var 4])
13719 line(start = [var 5, var 6], end = [var 7, var 8])
13720 line(start = [var 9, var 10], end = [var 11, var 12])
13721}
13722";
13723
13724 let program = Program::parse(initial_source).unwrap().0.unwrap();
13725
13726 let mut frontend = FrontendState::new();
13727
13728 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13729 let mock_ctx = ExecutorContext::new_mock(None).await;
13730 let version = Version(0);
13731
13732 frontend.hack_set_program(&ctx, program).await.unwrap();
13733 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13734 let sketch_id = sketch_object.id;
13735 let sketch = expect_sketch(sketch_object);
13736 let line1_id = *sketch.segments.get(2).unwrap();
13737 let line2_id = *sketch.segments.get(5).unwrap();
13738 let line3_id = *sketch.segments.get(8).unwrap();
13739
13740 let constraint = Constraint::Parallel(Parallel {
13741 lines: vec![line1_id, line2_id, line3_id],
13742 });
13743 let (src_delta, scene_delta) = frontend
13744 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13745 .await
13746 .unwrap();
13747 insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13748
13749 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13750 let sketch = expect_sketch(sketch_object);
13751 assert_eq!(sketch.constraints.len(), 1);
13752
13753 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13754 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13755 panic!("Expected constraint object");
13756 };
13757 let Constraint::Parallel(parallel) = constraint else {
13758 panic!("Expected parallel constraint");
13759 };
13760 assert_eq!(parallel.lines.len(), 3);
13761
13762 ctx.close().await;
13763 mock_ctx.close().await;
13764 }
13765
13766 #[tokio::test(flavor = "multi_thread")]
13767 async fn test_lines_perpendicular() {
13768 let initial_source = "\
13769sketch(on = XY) {
13770 line(start = [var 1, var 2], end = [var 3, var 4])
13771 line(start = [var 5, var 6], end = [var 7, var 8])
13772}
13773";
13774
13775 let program = Program::parse(initial_source).unwrap().0.unwrap();
13776
13777 let mut frontend = FrontendState::new();
13778
13779 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13780 let mock_ctx = ExecutorContext::new_mock(None).await;
13781 let version = Version(0);
13782
13783 frontend.hack_set_program(&ctx, program).await.unwrap();
13784 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13785 let sketch_id = sketch_object.id;
13786 let sketch = expect_sketch(sketch_object);
13787 let line1_id = *sketch.segments.get(2).unwrap();
13788 let line2_id = *sketch.segments.get(5).unwrap();
13789
13790 let constraint = Constraint::Perpendicular(Perpendicular {
13791 lines: vec![line1_id, line2_id],
13792 });
13793 let (src_delta, scene_delta) = frontend
13794 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13795 .await
13796 .unwrap();
13797 insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13798 assert_eq!(
13799 scene_delta.new_graph.objects.len(),
13800 9,
13801 "{:#?}",
13802 scene_delta.new_graph.objects
13803 );
13804
13805 ctx.close().await;
13806 mock_ctx.close().await;
13807 }
13808
13809 #[tokio::test(flavor = "multi_thread")]
13810 async fn test_lines_angle() {
13811 let initial_source = "\
13812sketch(on = XY) {
13813 line(start = [var 1, var 2], end = [var 3, var 4])
13814 line(start = [var 5, var 6], end = [var 7, var 8])
13815}
13816";
13817
13818 let program = Program::parse(initial_source).unwrap().0.unwrap();
13819
13820 let mut frontend = FrontendState::new();
13821
13822 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13823 let mock_ctx = ExecutorContext::new_mock(None).await;
13824 let version = Version(0);
13825
13826 frontend.hack_set_program(&ctx, program).await.unwrap();
13827 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13828 let sketch_id = sketch_object.id;
13829 let sketch = expect_sketch(sketch_object);
13830 let line1_id = *sketch.segments.get(2).unwrap();
13831 let line2_id = *sketch.segments.get(5).unwrap();
13832
13833 let constraint = Constraint::Angle(Angle {
13834 lines: vec![line1_id, line2_id],
13835 angle: Number {
13836 value: 30.0,
13837 units: NumericSuffix::Deg,
13838 },
13839 sector: None,
13840 inverse: None,
13841 label_position: None,
13842 source: Default::default(),
13843 });
13844 let (src_delta, scene_delta) = frontend
13845 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13846 .await
13847 .unwrap();
13848 insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13849 assert_eq!(
13850 scene_delta.new_graph.objects.len(),
13851 9,
13852 "{:#?}",
13853 scene_delta.new_graph.objects
13854 );
13855
13856 ctx.close().await;
13857 mock_ctx.close().await;
13858 }
13859
13860 #[tokio::test(flavor = "multi_thread")]
13861 async fn test_lines_angle_with_sector_uses_angle_dimension() {
13862 let initial_source = "\
13863sketch(on = XY) {
13864 line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13865 line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13866}
13867";
13868
13869 let program = Program::parse(initial_source).unwrap().0.unwrap();
13870
13871 let mut frontend = FrontendState::new();
13872
13873 let mock_ctx = ExecutorContext::new_mock(None).await;
13874 let version = Version(0);
13875
13876 frontend.program = program.clone();
13877 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13878 frontend.update_state_after_exec(outcome, true);
13879 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13880 let sketch_id = sketch_object.id;
13881 let sketch = expect_sketch(sketch_object);
13882 let line1_id = *sketch.segments.get(2).unwrap();
13883 let line2_id = *sketch.segments.get(5).unwrap();
13884
13885 let constraint = Constraint::Angle(Angle {
13886 lines: vec![line1_id, line2_id],
13887 angle: Number {
13888 value: 270.0,
13889 units: NumericSuffix::Deg,
13890 },
13891 sector: Some(1),
13892 inverse: Some(true),
13893 label_position: Some(Point2d {
13894 x: Number {
13895 value: -0.73,
13896 units: NumericSuffix::Mm,
13897 },
13898 y: Number {
13899 value: 0.75,
13900 units: NumericSuffix::Mm,
13901 },
13902 }),
13903 source: Default::default(),
13904 });
13905 let (src_delta, _) = frontend
13906 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13907 .await
13908 .unwrap();
13909 assert_eq!(
13910 src_delta.text.as_str(),
13911 "\
13912sketch(on = XY) {
13913 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13914 line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13915 angleDimension(
13916 lines = [line1, line2],
13917 sector = 1,
13918 inverse = true,
13919 labelPosition = [-0.73mm, 0.75mm],
13920) == 270deg
13921}
13922"
13923 );
13924
13925 mock_ctx.close().await;
13926 }
13927
13928 #[tokio::test(flavor = "multi_thread")]
13929 async fn test_segments_tangent() {
13930 let initial_source = "\
13931sketch(on = XY) {
13932 line(start = [var 1, var 2], end = [var 3, var 4])
13933 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13934}
13935";
13936
13937 let program = Program::parse(initial_source).unwrap().0.unwrap();
13938
13939 let mut frontend = FrontendState::new();
13940
13941 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13942 let mock_ctx = ExecutorContext::new_mock(None).await;
13943 let version = Version(0);
13944
13945 frontend.hack_set_program(&ctx, program).await.unwrap();
13946 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13947 let sketch_id = sketch_object.id;
13948 let sketch = expect_sketch(sketch_object);
13949 let line1_id = *sketch.segments.get(2).unwrap();
13950 let arc1_id = *sketch.segments.get(6).unwrap();
13951
13952 let constraint = Constraint::Tangent(Tangent {
13953 input: vec![line1_id, arc1_id],
13954 });
13955 let (src_delta, scene_delta) = frontend
13956 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13957 .await
13958 .unwrap();
13959 insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
13960 assert_eq!(
13961 scene_delta.new_graph.objects.len(),
13962 10,
13963 "{:#?}",
13964 scene_delta.new_graph.objects
13965 );
13966
13967 ctx.close().await;
13968 mock_ctx.close().await;
13969 }
13970
13971 #[tokio::test(flavor = "multi_thread")]
13972 async fn test_point_midpoint() {
13973 let initial_source = "\
13974sketch(on = XY) {
13975 point(at = [var 1, var 1])
13976 line(start = [var 0, var 0], end = [var 6, var 4])
13977}
13978";
13979
13980 let program = Program::parse(initial_source).unwrap().0.unwrap();
13981
13982 let mut frontend = FrontendState::new();
13983
13984 let ctx = ExecutorContext::new_mock(None).await;
13985 let version = Version(0);
13986
13987 frontend.program = program.clone();
13988 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13989 frontend.update_state_after_exec(outcome, true);
13990 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13991 let sketch_id = sketch_object.id;
13992 let sketch = expect_sketch(sketch_object);
13993 let point_id = *sketch.segments.first().unwrap();
13994 let line_id = *sketch.segments.get(3).unwrap();
13995
13996 let constraint = Constraint::Midpoint(Midpoint {
13997 point: ConstraintSegment::from(point_id),
13998 segment: line_id,
13999 });
14000 let (src_delta, scene_delta) = frontend
14001 .add_constraint(&ctx, version, sketch_id, constraint)
14002 .await
14003 .unwrap();
14004 insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
14005 assert_eq!(
14006 scene_delta.new_graph.objects.len(),
14007 7,
14008 "{:#?}",
14009 scene_delta.new_graph.objects
14010 );
14011
14012 ctx.close().await;
14013 }
14014
14015 #[tokio::test(flavor = "multi_thread")]
14016 async fn test_segments_symmetric() {
14017 let initial_source = "\
14018sketch(on = XY) {
14019 line(start = [var 0, var 0], end = [var 0, var 4])
14020 line(start = [var 4, var 0], end = [var 4, var 4])
14021 line(start = [var 2, var -1], end = [var 2, var 5])
14022}
14023";
14024
14025 let program = Program::parse(initial_source).unwrap().0.unwrap();
14026
14027 let mut frontend = FrontendState::new();
14028
14029 let ctx = ExecutorContext::new_mock(None).await;
14030 let version = Version(0);
14031
14032 frontend.program = program.clone();
14033 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14034 frontend.update_state_after_exec(outcome, true);
14035 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14036 let sketch_id = sketch_object.id;
14037 let sketch = expect_sketch(sketch_object);
14038 let line1_id = *sketch.segments.get(2).unwrap();
14039 let line2_id = *sketch.segments.get(5).unwrap();
14040 let axis_id = *sketch.segments.get(8).unwrap();
14041
14042 let constraint = Constraint::Symmetric(Symmetric {
14043 input: vec![line1_id, line2_id],
14044 axis: axis_id,
14045 });
14046 let (src_delta, scene_delta) = frontend
14047 .add_constraint(&ctx, version, sketch_id, constraint)
14048 .await
14049 .unwrap();
14050 insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14051 assert_eq!(
14052 scene_delta.new_graph.objects.len(),
14053 12,
14054 "{:#?}",
14055 scene_delta.new_graph.objects
14056 );
14057
14058 ctx.close().await;
14059 }
14060
14061 #[tokio::test(flavor = "multi_thread")]
14062 async fn test_point_arc_midpoint() {
14063 let initial_source = "\
14064sketch(on = XY) {
14065 point(at = [var 6, var 3])
14066 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14067}
14068";
14069
14070 let program = Program::parse(initial_source).unwrap().0.unwrap();
14071
14072 let mut frontend = FrontendState::new();
14073
14074 let ctx = ExecutorContext::new_mock(None).await;
14075 let version = Version(0);
14076
14077 frontend.program = program.clone();
14078 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14079 frontend.update_state_after_exec(outcome, true);
14080 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14081 let sketch_id = sketch_object.id;
14082 let sketch = expect_sketch(sketch_object);
14083 let point_id = *sketch.segments.first().unwrap();
14084 let arc_id = *sketch.segments.get(4).unwrap();
14085
14086 let constraint = Constraint::Midpoint(Midpoint {
14087 point: ConstraintSegment::from(point_id),
14088 segment: arc_id,
14089 });
14090 let (src_delta, scene_delta) = frontend
14091 .add_constraint(&ctx, version, sketch_id, constraint)
14092 .await
14093 .unwrap();
14094 insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14095 assert_eq!(
14096 scene_delta.new_graph.objects.len(),
14097 8,
14098 "{:#?}",
14099 scene_delta.new_graph.objects
14100 );
14101
14102 ctx.close().await;
14103 }
14104
14105 #[tokio::test(flavor = "multi_thread")]
14106 async fn test_origin_line_midpoint() {
14107 let initial_source = "\
14108sketch(on = XY) {
14109 line(start = [var 0, var 0], end = [var 6, var 4])
14110}
14111";
14112
14113 let program = Program::parse(initial_source).unwrap().0.unwrap();
14114
14115 let mut frontend = FrontendState::new();
14116
14117 let ctx = ExecutorContext::new_mock(None).await;
14118 let version = Version(0);
14119
14120 frontend.program = program.clone();
14121 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14122 frontend.update_state_after_exec(outcome, true);
14123 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14124 let sketch_id = sketch_object.id;
14125 let sketch = expect_sketch(sketch_object);
14126 let line_id = *sketch.segments.get(2).unwrap();
14127
14128 let constraint = Constraint::Midpoint(Midpoint {
14129 point: ConstraintSegment::ORIGIN,
14130 segment: line_id,
14131 });
14132 let (src_delta, scene_delta) = frontend
14133 .add_constraint(&ctx, version, sketch_id, constraint)
14134 .await
14135 .unwrap();
14136 insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14137 assert_eq!(
14138 scene_delta.new_graph.objects.len(),
14139 6,
14140 "{:#?}",
14141 scene_delta.new_graph.objects
14142 );
14143
14144 ctx.close().await;
14145 }
14146
14147 #[tokio::test(flavor = "multi_thread")]
14148 async fn test_origin_arc_midpoint() {
14149 let initial_source = "\
14150sketch(on = XY) {
14151 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14152}
14153";
14154
14155 let program = Program::parse(initial_source).unwrap().0.unwrap();
14156
14157 let mut frontend = FrontendState::new();
14158
14159 let ctx = ExecutorContext::new_mock(None).await;
14160 let version = Version(0);
14161
14162 frontend.program = program.clone();
14163 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14164 frontend.update_state_after_exec(outcome, true);
14165 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14166 let sketch_id = sketch_object.id;
14167 let sketch = expect_sketch(sketch_object);
14168 let arc_id = *sketch.segments.get(3).unwrap();
14169
14170 let constraint = Constraint::Midpoint(Midpoint {
14171 point: ConstraintSegment::ORIGIN,
14172 segment: arc_id,
14173 });
14174 let (src_delta, scene_delta) = frontend
14175 .add_constraint(&ctx, version, sketch_id, constraint)
14176 .await
14177 .unwrap();
14178 insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14179 assert_eq!(
14180 scene_delta.new_graph.objects.len(),
14181 7,
14182 "{:#?}",
14183 scene_delta.new_graph.objects
14184 );
14185
14186 ctx.close().await;
14187 }
14188
14189 #[tokio::test(flavor = "multi_thread")]
14190 async fn test_segments_symmetric_arcs() {
14191 let initial_source = "\
14192sketch(on = XY) {
14193 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14194 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14195 line(start = [var 0, var -10], end = [var 0, var 10])
14196}
14197";
14198
14199 let program = Program::parse(initial_source).unwrap().0.unwrap();
14200
14201 let mut frontend = FrontendState::new();
14202
14203 let ctx = ExecutorContext::new_mock(None).await;
14204 let version = Version(0);
14205
14206 frontend.program = program.clone();
14207 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14208 frontend.update_state_after_exec(outcome, true);
14209 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14210 let sketch_id = sketch_object.id;
14211 let sketch = expect_sketch(sketch_object);
14212 let arc1_id = *sketch.segments.get(3).unwrap();
14213 let arc2_id = *sketch.segments.get(7).unwrap();
14214 let axis_id = *sketch.segments.get(10).unwrap();
14215
14216 let constraint = Constraint::Symmetric(Symmetric {
14217 input: vec![arc1_id, arc2_id],
14218 axis: axis_id,
14219 });
14220 let (src_delta, scene_delta) = frontend
14221 .add_constraint(&ctx, version, sketch_id, constraint)
14222 .await
14223 .unwrap();
14224 insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14225 assert_eq!(
14226 scene_delta.new_graph.objects.len(),
14227 14,
14228 "{:#?}",
14229 scene_delta.new_graph.objects
14230 );
14231
14232 ctx.close().await;
14233 }
14234
14235 #[tokio::test(flavor = "multi_thread")]
14236 async fn test_sketch_on_face_simple() {
14237 let initial_source = "\
14238len = 2mm
14239cube = startSketchOn(XY)
14240 |> startProfile(at = [0, 0])
14241 |> line(end = [len, 0], tag = $side)
14242 |> line(end = [0, len])
14243 |> line(end = [-len, 0])
14244 |> line(end = [0, -len])
14245 |> close()
14246 |> extrude(length = len)
14247
14248face = faceOf(cube, face = side)
14249";
14250
14251 let program = Program::parse(initial_source).unwrap().0.unwrap();
14252
14253 let mut frontend = FrontendState::new();
14254
14255 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14256 let mock_ctx = ExecutorContext::new_mock(None).await;
14257 let version = Version(0);
14258
14259 frontend.hack_set_program(&ctx, program).await.unwrap();
14260 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14261 let face_id = face_object.id;
14262
14263 let sketch_args = SketchCtor {
14264 on: Plane::Object(face_id),
14265 };
14266 let (_src_delta, scene_delta, sketch_id) = frontend
14267 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14268 .await
14269 .unwrap();
14270 assert_eq!(sketch_id, ObjectId(2));
14271 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14272 let sketch_object = &scene_delta.new_graph.objects[2];
14273 assert_eq!(sketch_object.id, ObjectId(2));
14274 assert_eq!(
14275 sketch_object.kind,
14276 ObjectKind::Sketch(Sketch {
14277 args: SketchCtor {
14278 on: Plane::Object(face_id),
14279 },
14280 plane: face_id,
14281 segments: vec![],
14282 constraints: vec![],
14283 })
14284 );
14285 assert_eq!(scene_delta.new_graph.objects.len(), 8);
14286
14287 ctx.close().await;
14288 mock_ctx.close().await;
14289 }
14290
14291 #[tokio::test(flavor = "multi_thread")]
14292 async fn test_new_sketch_on_primitive_index_face() {
14293 let initial_source = "\
14294@settings(kclVersion = 2.0)
14295
14296sketch001 = sketch(on = XY) {
14297 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14298}
14299extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14300shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14301 let program = Program::parse(initial_source).unwrap().0.unwrap();
14302 let ctx = ExecutorContext::new_mock(None).await;
14303 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14304 let solid_id = match outcome.variables.get("shell001") {
14305 Some(KclValueView::Solid { value }) => value.id,
14306 value => panic!("expected shell001 to be a solid, got {value:?}"),
14307 };
14308 let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14309
14310 let mut ast = program.ast;
14311 let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14312 let face_expr = sketch_on_ast_expr(
14313 &mut ast,
14314 &scene_graph,
14315 &solid_references,
14316 &Plane::PrimitiveFace(crate::frontend::api::PrimitiveFacePlane { solid_id, index: 6 }),
14317 )
14318 .unwrap();
14319 let face_decl = ast::VariableDeclaration::new(
14320 ast::VariableDeclarator::new("face001", face_expr),
14321 ast::ItemVisibility::Default,
14322 ast::VariableKind::Const,
14323 );
14324 ast.body
14325 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14326 face_decl,
14327 ))));
14328 let face_source = source_from_ast(&ast);
14329 let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14330 insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14331
14332 let program = Program::parse(&new_source).unwrap().0.unwrap();
14333 ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14334 ctx.close().await;
14335 }
14336
14337 #[tokio::test(flavor = "multi_thread")]
14338 async fn test_sketch_on_wall_artifact_from_region_extrude() {
14339 let initial_source = "\
14340s = sketch(on = YZ) {
14341 line1 = line(start = [0, 0], end = [0, 1])
14342 line2 = line(start = [0, 1], end = [1, 1])
14343 line3 = line(start = [1, 1], end = [0, 0])
14344}
14345region001 = region(point = [0.1, 0.1], sketch = s)
14346extrude001 = extrude(region001, length = 5)
14347";
14348
14349 let program = Program::parse(initial_source).unwrap().0.unwrap();
14350
14351 let mut frontend = FrontendState::new();
14352 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14353 let version = Version(0);
14354
14355 frontend.hack_set_program(&ctx, program).await.unwrap();
14356 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14357
14358 let sketch_args = SketchCtor {
14359 on: Plane::Object(wall_object_id),
14360 };
14361 let (src_delta, _scene_delta, _sketch_id) = frontend
14362 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14363 .await
14364 .unwrap();
14365 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14366
14367 ctx.close().await;
14368 }
14369
14370 #[tokio::test(flavor = "multi_thread")]
14371 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14372 let initial_source = "\
14373sketch001 = sketch(on = YZ) {
14374 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14375 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14376 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14377 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14378 coincident([line1.end, line2.start])
14379 coincident([line2.end, line3.start])
14380 coincident([line3.end, line4.start])
14381 coincident([line4.end, line1.start])
14382 parallel([line2, line4])
14383 parallel([line3, line1])
14384 perpendicular([line1, line2])
14385 horizontal(line3)
14386 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14387}
14388region001 = region(point = [3.1, 3.74], sketch = sketch001)
14389extrude001 = extrude(region001, length = 5)
14390";
14391
14392 let program = Program::parse(initial_source).unwrap().0.unwrap();
14393
14394 let mut frontend = FrontendState::new();
14395 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14396 let version = Version(0);
14397
14398 frontend.hack_set_program(&ctx, program).await.unwrap();
14399 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14400
14401 let sketch_args = SketchCtor {
14402 on: Plane::Object(wall_object_id),
14403 };
14404 let (src_delta, _scene_delta, _sketch_id) = frontend
14405 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14406 .await
14407 .unwrap();
14408 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14409
14410 ctx.close().await;
14411 }
14412
14413 #[tokio::test(flavor = "multi_thread")]
14414 async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14415 let initial_source = "\
14416@settings(kclVersion = 2.0)
14417
14418sketch001 = sketch(on = XY) {
14419 circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14420 circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14421}
14422hidden001 = hide(sketch001)
14423region001 = region(segments = [sketch001.circle2])
14424region002 = region(segments = [sketch001.circle1])
14425extrude001 = extrude([region001, region002], length = 5)
14426";
14427
14428 let program = Program::parse(initial_source).unwrap().0.unwrap();
14429 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14430 let version = Version(0);
14431
14432 for (solid_output_index, expected_face) in [
14433 (0, "faceOf(extrude001[0], face = END)"),
14434 (1, "faceOf(extrude001[1], face = END)"),
14435 ] {
14436 let mut frontend = FrontendState::new();
14437 frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14438 let cap_object_id = find_cap_object_id_with_solid_output_index(
14439 &frontend.scene_graph,
14440 crate::frontend::api::CapKind::End,
14441 solid_output_index,
14442 )
14443 .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14444
14445 let sketch_args = SketchCtor {
14446 on: Plane::Object(cap_object_id),
14447 };
14448 let (src_delta, _scene_delta, _sketch_id) = frontend
14449 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14450 .await
14451 .unwrap();
14452
14453 assert!(
14454 src_delta.text.contains(expected_face),
14455 "expected `{expected_face}` in:\n{}",
14456 src_delta.text
14457 );
14458 assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14459 }
14460
14461 ctx.close().await;
14462 }
14463
14464 #[tokio::test(flavor = "multi_thread")]
14465 async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14466 let initial_source = "\
14467@settings(kclVersion = 2.0)
14468
14469sketch001 = sketch(on = XY) {
14470 rect1Line1 = line(start = [0, 0], end = [1, 0])
14471 rect1Line2 = line(start = [1, 0], end = [1, 1])
14472 rect1Line3 = line(start = [1, 1], end = [0, 1])
14473 rect1Line4 = line(start = [0, 1], end = [0, 0])
14474 rect2Line1 = line(start = [3, 0], end = [4, 0])
14475 rect2Line2 = line(start = [4, 0], end = [4, 1])
14476 rect2Line3 = line(start = [4, 1], end = [3, 1])
14477 rect2Line4 = line(start = [3, 1], end = [3, 0])
14478}
14479hidden001 = hide(sketch001)
14480region001 = region(segments = [
14481 sketch001.rect1Line4,
14482 sketch001.rect1Line1
14483])
14484region002 = region(segments = [
14485 sketch001.rect2Line4,
14486 sketch001.rect2Line1
14487])
14488extrude001 = extrude([region001, region002], length = 5)
14489";
14490
14491 let program = Program::parse(initial_source).unwrap().0.unwrap();
14492 let mut frontend = FrontendState::new();
14493 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14494 let version = Version(0);
14495
14496 frontend.hack_set_program(&ctx, program).await.unwrap();
14497 let region_call = "\
14498region(segments = [
14499 sketch001.rect1Line4,
14500 sketch001.rect1Line1
14501])";
14502 let region_call_start = initial_source.find(region_call).unwrap();
14503 let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14504 let segment_call = "line(start = [0, 0], end = [1, 0])";
14505 let segment_call_start = initial_source.find(segment_call).unwrap();
14506 let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14507 let wall_object_id = frontend
14508 .scene_graph
14509 .objects
14510 .iter()
14511 .find_map(|object| match &object.kind {
14512 ObjectKind::Wall(wall)
14513 if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14514 && wall.source.segment.range == segment_range =>
14515 {
14516 Some(object.id)
14517 }
14518 _ => None,
14519 })
14520 .expect("expected a wall object for region001.tags.rect1Line1");
14521
14522 let sketch_args = SketchCtor {
14523 on: Plane::Object(wall_object_id),
14524 };
14525 let (src_delta, _scene_delta, _sketch_id) = frontend
14526 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14527 .await
14528 .unwrap();
14529
14530 let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14531 assert!(
14532 src_delta.text.contains(expected_face),
14533 "expected `{expected_face}` in:\n{}",
14534 src_delta.text
14535 );
14536 assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14537
14538 ctx.close().await;
14539 }
14540
14541 #[test]
14542 fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14543 let source = "\
14544sketch001 = sketch(on = XY) {
14545 line(start = [0, 0], end = [1, 0])
14546}
14547part = subtract(boxSolid, tools = [cutSolid])
14548 |> appearance(color = \"#8f96a3\")
14549";
14550 let ast = Program::parse(source).unwrap().0.unwrap().ast;
14551 let line_start = source.find("line").unwrap();
14552 let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14553 let line_ref = SourceRef::Simple {
14554 range: [line_start, line_end, 0].into(),
14555 node_path: None,
14556 };
14557 assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14558
14559 let subtract_start = source.find("subtract").unwrap();
14560 let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14561 let subtract_ref = SourceRef::Simple {
14562 range: [subtract_start, subtract_end, 0].into(),
14563 node_path: None,
14564 };
14565 assert_eq!(
14566 variable_name_containing_source_ref(&ast, &subtract_ref),
14567 Some("part".to_owned())
14568 );
14569 }
14570
14571 #[tokio::test(flavor = "multi_thread")]
14572 async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14573 clear_mem_cache().await;
14574 let source = "\
14575boxSolid = startSketchOn(XY)
14576 |> startProfile(at = [0, 0])
14577 |> line(end = [4, 0], tag = $bottomEdge)
14578 |> line(end = [0, 4])
14579 |> line(end = [-4, 0])
14580 |> close()
14581 |> extrude(length = 10)
14582cutSolid = startSketchOn(XY)
14583 |> startProfile(at = [1, 1])
14584 |> line(end = [1, 0])
14585 |> line(end = [0, 1])
14586 |> line(end = [-1, 0])
14587 |> close()
14588 |> extrude(length = 10)
14589part = subtract(boxSolid, tools = [cutSolid])
14590 |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14591";
14592 let program = Program::parse(source).unwrap().0.unwrap();
14593 let mut frontend = FrontendState::new();
14594 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14595 match frontend.hack_set_program(&ctx, program).await.unwrap() {
14596 SetProgramOutcome::Success { .. } => {}
14597 SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14598 }
14599
14600 let sweep_call_start = source.find("extrude").unwrap();
14601 let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14602 let part_call_start = source.find("subtract").unwrap();
14603 let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14604 let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14605 let composite_range = [part_call_start, part_call_end, 0].into();
14606
14607 let cap_object = frontend
14608 .scene_graph
14609 .objects
14610 .iter()
14611 .find(|object| {
14612 matches!(
14613 &object.kind,
14614 ObjectKind::Cap(crate::frontend::api::Cap {
14615 kind: crate::frontend::api::CapKind::End,
14616 source,
14617 ..
14618 }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14619 )
14620 })
14621 .expect("expected end cap object to trace through subtract and original extrude");
14622
14623 let mut ast = frontend.program.ast.clone();
14624 let cap_expr = sketch_on_ast_expr(
14625 &mut ast,
14626 &frontend.scene_graph,
14627 &frontend.solid_references,
14628 &Plane::Object(cap_object.id),
14629 )
14630 .unwrap();
14631 let cap_face_decl = ast::VariableDeclaration::new(
14632 ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14633 ast::ItemVisibility::Default,
14634 ast::VariableKind::Const,
14635 );
14636 ast.body
14637 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14638 cap_face_decl,
14639 ))));
14640 let generated_source = source_from_ast(&ast);
14641
14642 assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14643 assert!(!generated_source.contains("faceOf(boxSolid"));
14644 let ast::Expr::CallExpressionKw(call) = cap_expr else {
14645 panic!("expected faceOf call");
14646 };
14647 assert_eq!(call.callee.name.name, "faceOf");
14648 let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14649 panic!("expected solid name");
14650 };
14651 assert_eq!(solid_name.name.name, "part");
14652 let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14653 panic!("expected face name");
14654 };
14655 assert_eq!(face_name.name.name, "END");
14656
14657 ctx.close().await;
14658 }
14659
14660 #[tokio::test(flavor = "multi_thread")]
14661 async fn test_sketch_on_plane_incremental() {
14662 let initial_source = "\
14663len = 2mm
14664cube = startSketchOn(XY)
14665 |> startProfile(at = [0, 0])
14666 |> line(end = [len, 0], tag = $side)
14667 |> line(end = [0, len])
14668 |> line(end = [-len, 0])
14669 |> line(end = [0, -len])
14670 |> close()
14671 |> extrude(length = len)
14672
14673plane = planeOf(cube, face = side)
14674";
14675
14676 let program = Program::parse(initial_source).unwrap().0.unwrap();
14677
14678 let mut frontend = FrontendState::new();
14679
14680 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14681 let mock_ctx = ExecutorContext::new_mock(None).await;
14682 let version = Version(0);
14683
14684 frontend.hack_set_program(&ctx, program).await.unwrap();
14685 let plane_object = frontend
14687 .scene_graph
14688 .objects
14689 .iter()
14690 .rev()
14691 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14692 .unwrap();
14693 let plane_id = plane_object.id;
14694
14695 let sketch_args = SketchCtor {
14696 on: Plane::Object(plane_id),
14697 };
14698 let (src_delta, scene_delta, sketch_id) = frontend
14699 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14700 .await
14701 .unwrap();
14702 insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14703 assert_eq!(sketch_id, ObjectId(2));
14704 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14705 let sketch_object = &scene_delta.new_graph.objects[2];
14706 assert_eq!(sketch_object.id, ObjectId(2));
14707 assert_eq!(
14708 sketch_object.kind,
14709 ObjectKind::Sketch(Sketch {
14710 args: SketchCtor {
14711 on: Plane::Object(plane_id),
14712 },
14713 plane: plane_id,
14714 segments: vec![],
14715 constraints: vec![],
14716 })
14717 );
14718 assert_eq!(scene_delta.new_graph.objects.len(), 9);
14719
14720 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14721 assert_eq!(plane_object.id, plane_id);
14722 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14723
14724 ctx.close().await;
14725 mock_ctx.close().await;
14726 }
14727
14728 #[tokio::test(flavor = "multi_thread")]
14729 async fn test_new_sketch_uses_unique_variable_name() {
14730 let initial_source = "\
14731sketch1 = sketch(on = XY) {
14732}
14733";
14734
14735 let program = Program::parse(initial_source).unwrap().0.unwrap();
14736
14737 let mut frontend = FrontendState::new();
14738 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14739 let version = Version(0);
14740
14741 frontend.hack_set_program(&ctx, program).await.unwrap();
14742
14743 let sketch_args = SketchCtor {
14744 on: Plane::Default(PlaneName::Yz),
14745 };
14746 let (src_delta, _, _) = frontend
14747 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14748 .await
14749 .unwrap();
14750
14751 insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14752
14753 ctx.close().await;
14754 }
14755
14756 #[tokio::test(flavor = "multi_thread")]
14757 async fn test_new_sketch_twice_using_same_plane() {
14758 let initial_source = "\
14759sketch1 = sketch(on = XY) {
14760}
14761";
14762
14763 let program = Program::parse(initial_source).unwrap().0.unwrap();
14764
14765 let mut frontend = FrontendState::new();
14766 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14767 let version = Version(0);
14768
14769 frontend.hack_set_program(&ctx, program).await.unwrap();
14770
14771 let sketch_args = SketchCtor {
14772 on: Plane::Default(PlaneName::Xy),
14773 };
14774 let (src_delta, _, _) = frontend
14775 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14776 .await
14777 .unwrap();
14778
14779 insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14780
14781 ctx.close().await;
14782 }
14783
14784 #[tokio::test(flavor = "multi_thread")]
14785 async fn test_sketch_mode_reuses_cached_on_expression() {
14786 let initial_source = "\
14787width = 2mm
14788sketch(on = offsetPlane(XY, offset = width)) {
14789 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14790 distance([line1.start, line1.end]) == width
14791}
14792";
14793 let program = Program::parse(initial_source).unwrap().0.unwrap();
14794
14795 let mut frontend = FrontendState::new();
14796 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14797 let mock_ctx = ExecutorContext::new_mock(None).await;
14798 let version = Version(0);
14799 let project_id = ProjectId(0);
14800 let file_id = FileId(0);
14801
14802 frontend.hack_set_program(&ctx, program).await.unwrap();
14803 let initial_object_count = frontend.scene_graph.objects.len();
14804 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14805 .expect("Expected sketch object to exist")
14806 .id;
14807
14808 let scene_delta = frontend
14811 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14812 .await
14813 .unwrap();
14814 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14815
14816 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14819 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14820
14821 ctx.close().await;
14822 mock_ctx.close().await;
14823 }
14824
14825 #[tokio::test(flavor = "multi_thread")]
14826 async fn test_edit_sketch_nested_in_pipe() {
14827 clear_mem_cache().await;
14828 let source = r#"
14829profile = sketch(on = XY) {
14830 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14831}
14832 |> translate(x = 2mm)
14833"#;
14834 let program = Program::parse_no_errs(source).unwrap();
14835 let mut frontend = FrontendState::new();
14836 let mock_ctx = ExecutorContext::new_mock(None).await;
14837 let version = Version(0);
14838
14839 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14840 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14841 .expect("Expected piped sketch object")
14842 .id;
14843
14844 let scene_delta = frontend
14845 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14846 .await
14847 .unwrap();
14848 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14849 assert!(
14850 scene_delta
14851 .new_graph
14852 .objects
14853 .iter()
14854 .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14855 "Expected the piped sketch's segments to be present in sketch mode"
14856 );
14857
14858 clear_mem_cache().await;
14859 mock_ctx.close().await;
14860 }
14861
14862 #[tokio::test(flavor = "multi_thread")]
14863 async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14864 clear_mem_cache().await;
14865 let source = r#"
14866useFirstProfile = true
14867
14868profile = if useFirstProfile {
14869 sketch(on = XY) {
14870 line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14871 }
14872} else {
14873 sketch(on = XY) {
14874 line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14875 }
14876}
14877"#;
14878 let program = Program::parse_no_errs(source).unwrap();
14879 let mut frontend = FrontendState::new();
14880 let mock_ctx = ExecutorContext::new_mock(None).await;
14881 let version = Version(0);
14882
14883 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14884 let sketch_object =
14885 find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14886 let sketch_id = sketch_object.id;
14887 let sketch = expect_sketch(sketch_object);
14888 let line_end_id = *sketch
14889 .segments
14890 .get(1)
14891 .expect("Expected the active branch's line end point");
14892
14893 let scene_delta = frontend
14894 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14895 .await
14896 .unwrap();
14897 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14898
14899 let segments = vec![ExistingSegmentCtor {
14900 id: line_end_id,
14901 ctor: SegmentCtor::Point(PointCtor {
14902 position: Point2d {
14903 x: Expr::Var(Number {
14904 value: 30.0,
14905 units: NumericSuffix::Mm,
14906 }),
14907 y: Expr::Var(Number {
14908 value: 15.0,
14909 units: NumericSuffix::Mm,
14910 }),
14911 },
14912 }),
14913 }];
14914 let (source_delta, _) = frontend
14915 .edit_segments(&mock_ctx, version, sketch_id, segments)
14916 .await
14917 .unwrap();
14918 assert!(
14919 source_delta
14920 .text
14921 .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14922 "Expected the active branch's dragged variables to be updated:\n{}",
14923 source_delta.text
14924 );
14925 assert!(
14926 source_delta
14927 .text
14928 .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14929 "Expected the inactive branch to remain unchanged:\n{}",
14930 source_delta.text
14931 );
14932
14933 clear_mem_cache().await;
14934 mock_ctx.close().await;
14935 }
14936
14937 #[tokio::test(flavor = "multi_thread")]
14938 async fn test_multiple_sketch_blocks() {
14939 let initial_source = "\
14940// Cube that requires the engine.
14941width = 2
14942sketch001 = startSketchOn(XY)
14943profile001 = startProfile(sketch001, at = [0, 0])
14944 |> yLine(length = width, tag = $seg1)
14945 |> xLine(length = width)
14946 |> yLine(length = -width)
14947 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14948 |> close()
14949extrude001 = extrude(profile001, length = width)
14950
14951// Get a value that requires the engine.
14952x = segLen(seg1)
14953
14954// Triangle with side length 2*x.
14955sketch(on = XY) {
14956 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14957 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14958 coincident([line1.end, line2.start])
14959 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14960 coincident([line2.end, line3.start])
14961 coincident([line3.end, line1.start])
14962 equalLength([line3, line1])
14963 equalLength([line1, line2])
14964 distance([line1.start, line1.end]) == 2*x
14965}
14966
14967// Line segment with length x.
14968sketch2 = sketch(on = XY) {
14969 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14970 distance([line1.start, line1.end]) == x
14971}
14972";
14973
14974 let program = Program::parse(initial_source).unwrap().0.unwrap();
14975
14976 let mut frontend = FrontendState::new();
14977
14978 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14979 let mock_ctx = ExecutorContext::new_mock(None).await;
14980 let version = Version(0);
14981 let project_id = ProjectId(0);
14982 let file_id = FileId(0);
14983
14984 frontend.hack_set_program(&ctx, program).await.unwrap();
14985 let sketch_objects = frontend
14986 .scene_graph
14987 .objects
14988 .iter()
14989 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14990 .collect::<Vec<_>>();
14991 let sketch1_id = sketch_objects.first().unwrap().id;
14992 let sketch2_id = sketch_objects.get(1).unwrap().id;
14993 let point1_id = ObjectId(sketch1_id.0 + 1);
14995 let point2_id = ObjectId(sketch2_id.0 + 1);
14997
14998 let scene_delta = frontend
15007 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15008 .await
15009 .unwrap();
15010 assert_eq!(
15011 scene_delta.new_graph.objects.len(),
15012 18,
15013 "{:#?}",
15014 scene_delta.new_graph.objects
15015 );
15016
15017 let point_ctor = PointCtor {
15019 position: Point2d {
15020 x: Expr::Var(Number {
15021 value: 1.0,
15022 units: NumericSuffix::Mm,
15023 }),
15024 y: Expr::Var(Number {
15025 value: 2.0,
15026 units: NumericSuffix::Mm,
15027 }),
15028 },
15029 };
15030 let segments = vec![ExistingSegmentCtor {
15031 id: point1_id,
15032 ctor: SegmentCtor::Point(point_ctor),
15033 }];
15034 let (src_delta, _) = frontend
15035 .edit_segments(&mock_ctx, version, sketch1_id, segments)
15036 .await
15037 .unwrap();
15038 insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15040 let edited_sketch1_source = src_delta.text.clone();
15041
15042 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15044 assert_eq!(src_delta.text, edited_sketch1_source);
15045 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15053 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15054
15055 let scene_delta = frontend
15063 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15064 .await
15065 .unwrap();
15066 assert_eq!(
15067 scene_delta.new_graph.objects.len(),
15068 24,
15069 "{:#?}",
15070 scene_delta.new_graph.objects
15071 );
15072
15073 let point_ctor = PointCtor {
15075 position: Point2d {
15076 x: Expr::Var(Number {
15077 value: 3.0,
15078 units: NumericSuffix::Mm,
15079 }),
15080 y: Expr::Var(Number {
15081 value: 4.0,
15082 units: NumericSuffix::Mm,
15083 }),
15084 },
15085 };
15086 let segments = vec![ExistingSegmentCtor {
15087 id: point2_id,
15088 ctor: SegmentCtor::Point(point_ctor),
15089 }];
15090 let (src_delta, _) = frontend
15091 .edit_segments(&mock_ctx, version, sketch2_id, segments)
15092 .await
15093 .unwrap();
15094 insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15096 let edited_sketch2_source = src_delta.text.clone();
15097
15098 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15100 assert_eq!(src_delta.text, edited_sketch2_source);
15101
15102 ctx.close().await;
15103 mock_ctx.close().await;
15104 }
15105
15106 #[tokio::test(flavor = "multi_thread")]
15107 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15108 clear_mem_cache().await;
15109
15110 let source = r#"sketch001 = sketch(on = XZ) {
15111 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15112}
15113sketch002 = sketch(on = XY) {
15114 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15115 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15116 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15117 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15118 coincident([line1.end, line2.start])
15119 coincident([line2.end, line3.start])
15120 coincident([line3.end, line4.start])
15121 coincident([line4.end, line1.start])
15122 parallel([line2, line4])
15123 parallel([line3, line1])
15124 perpendicular([line1, line2])
15125 horizontal(line3)
15126 coincident([line1.start, ORIGIN])
15127}
15128"#;
15129
15130 let program = Program::parse(source).unwrap().0.unwrap();
15131 let mut frontend = FrontendState::new();
15132 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15133 let mock_ctx = ExecutorContext::new_mock(None).await;
15134 let version = Version(0);
15135 let project_id = ProjectId(0);
15136 let file_id = FileId(0);
15137
15138 frontend.hack_set_program(&ctx, program).await.unwrap();
15139 let sketch_objects = frontend
15140 .scene_graph
15141 .objects
15142 .iter()
15143 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15144 .collect::<Vec<_>>();
15145 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15146
15147 let sketch1_id = sketch_objects[0].id;
15148 let sketch2_id = sketch_objects[1].id;
15149
15150 frontend
15151 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15152 .await
15153 .unwrap();
15154 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15155
15156 let scene_delta = frontend
15157 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15158 .await
15159 .unwrap();
15160 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15161
15162 clear_mem_cache().await;
15163 ctx.close().await;
15164 mock_ctx.close().await;
15165 }
15166
15167 #[tokio::test(flavor = "multi_thread")]
15172 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15173 let initial_source = "@settings(defaultLengthUnit = mm)
15175
15176sketch001 = sketch(on = XY) {
15177 point(at = [1in, 2in])
15178}
15179";
15180
15181 let program = Program::parse(initial_source).unwrap().0.unwrap();
15182 let mut frontend = FrontendState::new();
15183
15184 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15185 let mock_ctx = ExecutorContext::new_mock(None).await;
15186 let version = Version(0);
15187 let project_id = ProjectId(0);
15188 let file_id = FileId(0);
15189
15190 frontend.hack_set_program(&ctx, program).await.unwrap();
15191 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15192 let sketch_id = sketch_object.id;
15193
15194 frontend
15196 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15197 .await
15198 .unwrap();
15199
15200 let point_ctor = PointCtor {
15202 position: Point2d {
15203 x: Expr::Number(Number {
15204 value: 5.0,
15205 units: NumericSuffix::Mm,
15206 }),
15207 y: Expr::Number(Number {
15208 value: 6.0,
15209 units: NumericSuffix::Mm,
15210 }),
15211 },
15212 };
15213 let segment = SegmentCtor::Point(point_ctor);
15214 let (src_delta, scene_delta) = frontend
15215 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15216 .await
15217 .unwrap();
15218 assert!(
15220 src_delta.text.contains("point(at = [5mm, 6mm])"),
15221 "Expected new point in source, got: {}",
15222 src_delta.text
15223 );
15224 assert!(!scene_delta.new_objects.is_empty());
15225
15226 ctx.close().await;
15227 mock_ctx.close().await;
15228 }
15229
15230 #[tokio::test(flavor = "multi_thread")]
15231 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15232 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15233
15234 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15235 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15236
15237 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15238 assert!(
15239 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15240 "Expected experimental settings to be added to source"
15241 );
15242 }
15243
15244 #[tokio::test(flavor = "multi_thread")]
15245 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15246 let initial_source = "@settings(defaultLengthUnit = mm)
15248
15249s = sketch(on = XY) {}
15250";
15251
15252 let program = Program::parse(initial_source).unwrap().0.unwrap();
15253 let mut frontend = FrontendState::new();
15254
15255 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15256 let mock_ctx = ExecutorContext::new_mock(None).await;
15257 let version = Version(0);
15258
15259 frontend.hack_set_program(&ctx, program).await.unwrap();
15260 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15261 let sketch_id = sketch_object.id;
15262
15263 let line_ctor = LineCtor {
15264 start: Point2d {
15265 x: Expr::Number(Number {
15266 value: 0.0,
15267 units: NumericSuffix::Mm,
15268 }),
15269 y: Expr::Number(Number {
15270 value: 0.0,
15271 units: NumericSuffix::Mm,
15272 }),
15273 },
15274 end: Point2d {
15275 x: Expr::Number(Number {
15276 value: 10.0,
15277 units: NumericSuffix::Mm,
15278 }),
15279 y: Expr::Number(Number {
15280 value: 10.0,
15281 units: NumericSuffix::Mm,
15282 }),
15283 },
15284 construction: None,
15285 };
15286 let segment = SegmentCtor::Line(line_ctor);
15287 let (src_delta, scene_delta) = frontend
15288 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15289 .await
15290 .unwrap();
15291 assert!(
15292 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15293 "Expected line in source, got: {}",
15294 src_delta.text
15295 );
15296 assert_eq!(scene_delta.new_objects.len(), 3);
15298
15299 ctx.close().await;
15300 mock_ctx.close().await;
15301 }
15302
15303 #[tokio::test(flavor = "multi_thread")]
15304 async fn test_extra_newlines_between_operations_edit_line() {
15305 let initial_source = "@settings(defaultLengthUnit = mm)
15307
15308sketch001 = sketch(on = XY) {
15309
15310 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15311
15312}
15313";
15314
15315 let program = Program::parse(initial_source).unwrap().0.unwrap();
15316 let mut frontend = FrontendState::new();
15317
15318 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15319 let mock_ctx = ExecutorContext::new_mock(None).await;
15320 let version = Version(0);
15321 let project_id = ProjectId(0);
15322 let file_id = FileId(0);
15323
15324 let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
15325 assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
15326 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15327 let sketch_id = sketch_object.id;
15328 let sketch = expect_sketch(sketch_object);
15329
15330 let line_id = sketch
15332 .segments
15333 .iter()
15334 .copied()
15335 .find(|seg_id| {
15336 matches!(
15337 &frontend.scene_graph.objects[seg_id.0].kind,
15338 ObjectKind::Segment {
15339 segment: Segment::Line(_)
15340 }
15341 )
15342 })
15343 .expect("Expected a line segment in sketch");
15344
15345 frontend
15347 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15348 .await
15349 .unwrap();
15350
15351 let line_ctor = LineCtor {
15353 start: Point2d {
15354 x: Expr::Var(Number {
15355 value: 1.0,
15356 units: NumericSuffix::Mm,
15357 }),
15358 y: Expr::Var(Number {
15359 value: 2.0,
15360 units: NumericSuffix::Mm,
15361 }),
15362 },
15363 end: Point2d {
15364 x: Expr::Var(Number {
15365 value: 13.0,
15366 units: NumericSuffix::Mm,
15367 }),
15368 y: Expr::Var(Number {
15369 value: 14.0,
15370 units: NumericSuffix::Mm,
15371 }),
15372 },
15373 construction: None,
15374 };
15375 let segments = vec![ExistingSegmentCtor {
15376 id: line_id,
15377 ctor: SegmentCtor::Line(line_ctor),
15378 }];
15379 let (src_delta, _scene_delta) = frontend
15380 .edit_segments(&mock_ctx, version, sketch_id, segments)
15381 .await
15382 .unwrap();
15383 assert!(
15384 src_delta
15385 .text
15386 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15387 "Expected edited line in source, got: {}",
15388 src_delta.text
15389 );
15390
15391 ctx.close().await;
15392 mock_ctx.close().await;
15393 }
15394
15395 #[tokio::test(flavor = "multi_thread")]
15396 async fn test_extra_newlines_delete_segment() {
15397 let initial_source = "@settings(defaultLengthUnit = mm)
15399
15400sketch001 = sketch(on = XY) {
15401 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15402}
15403";
15404
15405 let program = Program::parse(initial_source).unwrap().0.unwrap();
15406 let mut frontend = FrontendState::new();
15407
15408 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15409 let mock_ctx = ExecutorContext::new_mock(None).await;
15410 let version = Version(0);
15411
15412 frontend.hack_set_program(&ctx, program).await.unwrap();
15413 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15414 let sketch_id = sketch_object.id;
15415 let sketch = expect_sketch(sketch_object);
15416
15417 assert_eq!(sketch.segments.len(), 3);
15419 let circle_id = sketch.segments[2];
15420
15421 let (src_delta, scene_delta) = frontend
15423 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15424 .await
15425 .unwrap();
15426 assert!(
15427 src_delta.text.contains("sketch(on = XY) {"),
15428 "Expected sketch block in source, got: {}",
15429 src_delta.text
15430 );
15431 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15432 let new_sketch = expect_sketch(new_sketch_object);
15433 assert_eq!(new_sketch.segments.len(), 0);
15434
15435 ctx.close().await;
15436 mock_ctx.close().await;
15437 }
15438
15439 #[tokio::test(flavor = "multi_thread")]
15440 async fn test_unformatted_source_add_arc() {
15441 let initial_source = "@settings(defaultLengthUnit = mm)
15443
15444sketch001 = sketch(on = XY) {
15445}
15446";
15447
15448 let program = Program::parse(initial_source).unwrap().0.unwrap();
15449 let mut frontend = FrontendState::new();
15450
15451 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15452 let mock_ctx = ExecutorContext::new_mock(None).await;
15453 let version = Version(0);
15454
15455 frontend.hack_set_program(&ctx, program).await.unwrap();
15456 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15457 let sketch_id = sketch_object.id;
15458
15459 let arc_ctor = ArcCtor {
15460 start: Point2d {
15461 x: Expr::Var(Number {
15462 value: 5.0,
15463 units: NumericSuffix::Mm,
15464 }),
15465 y: Expr::Var(Number {
15466 value: 0.0,
15467 units: NumericSuffix::Mm,
15468 }),
15469 },
15470 end: Point2d {
15471 x: Expr::Var(Number {
15472 value: 0.0,
15473 units: NumericSuffix::Mm,
15474 }),
15475 y: Expr::Var(Number {
15476 value: 5.0,
15477 units: NumericSuffix::Mm,
15478 }),
15479 },
15480 center: Point2d {
15481 x: Expr::Var(Number {
15482 value: 0.0,
15483 units: NumericSuffix::Mm,
15484 }),
15485 y: Expr::Var(Number {
15486 value: 0.0,
15487 units: NumericSuffix::Mm,
15488 }),
15489 },
15490 direction: None,
15491 construction: None,
15492 };
15493 let segment = SegmentCtor::Arc(arc_ctor);
15494 let (src_delta, scene_delta) = frontend
15495 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15496 .await
15497 .unwrap();
15498 assert!(
15499 src_delta
15500 .text
15501 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15502 "Expected arc in source, got: {}",
15503 src_delta.text
15504 );
15505 assert!(!scene_delta.new_objects.is_empty());
15506
15507 ctx.close().await;
15508 mock_ctx.close().await;
15509 }
15510
15511 #[tokio::test(flavor = "multi_thread")]
15512 async fn test_arc_direction_flows_to_source() {
15513 let initial_source = "@settings(defaultLengthUnit = mm)
15514
15515sketch001 = sketch(on = XY) {
15516}
15517";
15518
15519 let program = Program::parse(initial_source).unwrap().0.unwrap();
15520 let mut frontend = FrontendState::new();
15521
15522 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15523 let mock_ctx = ExecutorContext::new_mock(None).await;
15524 let version = Version(0);
15525
15526 frontend.hack_set_program(&ctx, program).await.unwrap();
15527 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15528 let sketch_id = sketch_object.id;
15529
15530 let point = |x: f64, y: f64| Point2d {
15531 x: Expr::Var(Number {
15532 value: x,
15533 units: NumericSuffix::Mm,
15534 }),
15535 y: Expr::Var(Number {
15536 value: y,
15537 units: NumericSuffix::Mm,
15538 }),
15539 };
15540
15541 let arc_ctor = ArcCtor {
15543 start: point(5.0, 0.0),
15544 end: point(0.0, 5.0),
15545 center: point(0.0, 0.0),
15546 direction: Some(ArcDirection::Cw),
15547 construction: None,
15548 };
15549 let (src_delta, scene_delta) = frontend
15550 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15551 .await
15552 .unwrap();
15553 assert!(
15554 src_delta.text.contains("direction = CW"),
15555 "Expected direction = CW in source, got: {}",
15556 src_delta.text
15557 );
15558 let arc_id = *scene_delta.new_objects.last().unwrap();
15560
15561 let edited_ctor = ArcCtor {
15565 start: point(0.0, -5.0),
15566 end: point(0.0, 5.0),
15567 center: point(0.0, 0.0),
15568 direction: Some(ArcDirection::Cw),
15569 construction: None,
15570 };
15571 let (src_delta, _scene_delta) = frontend
15572 .edit_segments(
15573 &mock_ctx,
15574 version,
15575 sketch_id,
15576 vec![ExistingSegmentCtor {
15577 id: arc_id,
15578 ctor: SegmentCtor::Arc(edited_ctor),
15579 }],
15580 )
15581 .await
15582 .unwrap();
15583 assert!(
15584 src_delta.text.contains("start = [var 0mm, var -5mm]"),
15585 "Expected edited start point in source, got: {}",
15586 src_delta.text
15587 );
15588 assert!(
15589 src_delta.text.contains("direction = CW"),
15590 "Expected direction = CW to be preserved in source, got: {}",
15591 src_delta.text
15592 );
15593
15594 let edited_ctor = ArcCtor {
15597 start: point(0.0, -5.0),
15598 end: point(0.0, 5.0),
15599 center: point(0.0, 0.0),
15600 direction: Some(ArcDirection::Ccw),
15601 construction: None,
15602 };
15603 let (src_delta, _scene_delta) = frontend
15604 .edit_segments(
15605 &mock_ctx,
15606 version,
15607 sketch_id,
15608 vec![ExistingSegmentCtor {
15609 id: arc_id,
15610 ctor: SegmentCtor::Arc(edited_ctor),
15611 }],
15612 )
15613 .await
15614 .unwrap();
15615 assert!(
15616 !src_delta.text.contains("direction"),
15617 "Expected direction argument to be removed from source, got: {}",
15618 src_delta.text
15619 );
15620
15621 ctx.close().await;
15622 mock_ctx.close().await;
15623 }
15624
15625 #[tokio::test(flavor = "multi_thread")]
15626 async fn test_extra_newlines_add_circle() {
15627 let initial_source = "@settings(defaultLengthUnit = mm)
15629
15630sketch001 = sketch(on = XY) {
15631}
15632";
15633
15634 let program = Program::parse(initial_source).unwrap().0.unwrap();
15635 let mut frontend = FrontendState::new();
15636
15637 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15638 let mock_ctx = ExecutorContext::new_mock(None).await;
15639 let version = Version(0);
15640
15641 frontend.hack_set_program(&ctx, program).await.unwrap();
15642 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15643 let sketch_id = sketch_object.id;
15644
15645 let circle_ctor = CircleCtor {
15646 start: Point2d {
15647 x: Expr::Var(Number {
15648 value: 5.0,
15649 units: NumericSuffix::Mm,
15650 }),
15651 y: Expr::Var(Number {
15652 value: 0.0,
15653 units: NumericSuffix::Mm,
15654 }),
15655 },
15656 center: Point2d {
15657 x: Expr::Var(Number {
15658 value: 0.0,
15659 units: NumericSuffix::Mm,
15660 }),
15661 y: Expr::Var(Number {
15662 value: 0.0,
15663 units: NumericSuffix::Mm,
15664 }),
15665 },
15666 construction: None,
15667 };
15668 let segment = SegmentCtor::Circle(circle_ctor);
15669 let (src_delta, scene_delta) = frontend
15670 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15671 .await
15672 .unwrap();
15673 assert!(
15674 src_delta
15675 .text
15676 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15677 "Expected circle in source, got: {}",
15678 src_delta.text
15679 );
15680 assert!(!scene_delta.new_objects.is_empty());
15681
15682 ctx.close().await;
15683 mock_ctx.close().await;
15684 }
15685
15686 #[tokio::test(flavor = "multi_thread")]
15687 async fn test_extra_newlines_add_constraint() {
15688 let initial_source = "@settings(defaultLengthUnit = mm)
15690
15691sketch001 = sketch(on = XY) {
15692 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15693 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15694}
15695";
15696
15697 let program = Program::parse(initial_source).unwrap().0.unwrap();
15698 let mut frontend = FrontendState::new();
15699
15700 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15701 let mock_ctx = ExecutorContext::new_mock(None).await;
15702 let version = Version(0);
15703 let project_id = ProjectId(0);
15704 let file_id = FileId(0);
15705
15706 frontend.hack_set_program(&ctx, program).await.unwrap();
15707 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15708 let sketch_id = sketch_object.id;
15709 let sketch = expect_sketch(sketch_object);
15710
15711 let line_ids: Vec<ObjectId> = sketch
15713 .segments
15714 .iter()
15715 .copied()
15716 .filter(|seg_id| {
15717 matches!(
15718 &frontend.scene_graph.objects[seg_id.0].kind,
15719 ObjectKind::Segment {
15720 segment: Segment::Line(_)
15721 }
15722 )
15723 })
15724 .collect();
15725 assert_eq!(line_ids.len(), 2, "Expected two line segments");
15726
15727 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15728 let ObjectKind::Segment {
15729 segment: Segment::Line(line1_data),
15730 } = &line1.kind
15731 else {
15732 panic!("Expected line");
15733 };
15734 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15735 let ObjectKind::Segment {
15736 segment: Segment::Line(line2_data),
15737 } = &line2.kind
15738 else {
15739 panic!("Expected line");
15740 };
15741
15742 let constraint = Constraint::Coincident(Coincident {
15744 segments: vec![line1_data.end.into(), line2_data.start.into()],
15745 });
15746
15747 frontend
15749 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15750 .await
15751 .unwrap();
15752 let (src_delta, _scene_delta) = frontend
15753 .add_constraint(&mock_ctx, version, sketch_id, constraint)
15754 .await
15755 .unwrap();
15756 assert!(
15757 src_delta.text.contains("coincident("),
15758 "Expected coincident constraint in source, got: {}",
15759 src_delta.text
15760 );
15761
15762 ctx.close().await;
15763 mock_ctx.close().await;
15764 }
15765
15766 #[tokio::test(flavor = "multi_thread")]
15767 async fn test_extra_newlines_add_line_then_edit_line() {
15768 let initial_source = "@settings(defaultLengthUnit = mm)
15770
15771sketch001 = sketch(on = XY) {
15772}
15773";
15774
15775 let program = Program::parse(initial_source).unwrap().0.unwrap();
15776 let mut frontend = FrontendState::new();
15777
15778 let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15779 let mock_ctx = ExecutorContext::new_mock(None).await;
15780 let version = Version(0);
15781
15782 frontend.hack_set_program(&ctx, program).await.unwrap();
15783 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15784 let sketch_id = sketch_object.id;
15785
15786 let line_ctor = LineCtor {
15788 start: Point2d {
15789 x: Expr::Number(Number {
15790 value: 0.0,
15791 units: NumericSuffix::Mm,
15792 }),
15793 y: Expr::Number(Number {
15794 value: 0.0,
15795 units: NumericSuffix::Mm,
15796 }),
15797 },
15798 end: Point2d {
15799 x: Expr::Number(Number {
15800 value: 10.0,
15801 units: NumericSuffix::Mm,
15802 }),
15803 y: Expr::Number(Number {
15804 value: 10.0,
15805 units: NumericSuffix::Mm,
15806 }),
15807 },
15808 construction: None,
15809 };
15810 let segment = SegmentCtor::Line(line_ctor);
15811 let (src_delta, scene_delta) = frontend
15812 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15813 .await
15814 .unwrap();
15815 assert!(
15816 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15817 "Expected line in source after add, got: {}",
15818 src_delta.text
15819 );
15820 let line_id = *scene_delta.new_objects.last().unwrap();
15822
15823 let line_ctor = LineCtor {
15825 start: Point2d {
15826 x: Expr::Number(Number {
15827 value: 1.0,
15828 units: NumericSuffix::Mm,
15829 }),
15830 y: Expr::Number(Number {
15831 value: 2.0,
15832 units: NumericSuffix::Mm,
15833 }),
15834 },
15835 end: Point2d {
15836 x: Expr::Number(Number {
15837 value: 13.0,
15838 units: NumericSuffix::Mm,
15839 }),
15840 y: Expr::Number(Number {
15841 value: 14.0,
15842 units: NumericSuffix::Mm,
15843 }),
15844 },
15845 construction: None,
15846 };
15847 let segments = vec![ExistingSegmentCtor {
15848 id: line_id,
15849 ctor: SegmentCtor::Line(line_ctor),
15850 }];
15851 let (src_delta, scene_delta) = frontend
15852 .edit_segments(&mock_ctx, version, sketch_id, segments)
15853 .await
15854 .unwrap();
15855 assert!(
15856 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15857 "Expected edited line in source, got: {}",
15858 src_delta.text
15859 );
15860 assert_eq!(scene_delta.new_objects, vec![]);
15861
15862 ctx.close().await;
15863 mock_ctx.close().await;
15864 }
15865
15866 #[test]
15867 fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15868 let code = "\
15877foo1 = 1
15878sk = sketch() {
15879 p = var 1.5
15880}
158817 + 8
15882";
15883 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15884 let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15885 panic!("expected an expression statement");
15886 };
15887 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15888 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15889 &mut ast,
15890 &source_ref,
15891 AstMutateCommand::AddVariableDeclaration {
15892 prefix: "foo".to_owned(),
15893 },
15894 )
15895 .unwrap();
15896 let AstMutateCommandReturn::Name(name) = cmd_return else {
15897 panic!("expected a generated name");
15898 };
15899 assert_eq!(name, "foo2");
15900 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15901 panic!("expected the expression statement to become a variable declaration");
15902 };
15903 assert_eq!(decl.name(), "foo2");
15904 }
15905
15906 fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15908 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15909 panic!("expected a variable declaration");
15910 };
15911 let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15912 panic!("expected a function expression");
15913 };
15914 &func.body
15915 }
15916
15917 fn then_block_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15920 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15921 panic!("expected a variable declaration");
15922 };
15923 let ast::Expr::IfExpression(if_expr) = &decl.declaration.init else {
15924 panic!("expected an if expression");
15925 };
15926 &if_expr.then_val
15927 }
15928
15929 #[test]
15930 fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15931 let code = "\
15936fn build() {
15937 thing1 = 1
15938 10 + 20
15939 return thing1
15940}
15941";
15942 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15943 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15944 panic!("expected an expression statement");
15945 };
15946 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15947 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15948 &mut ast,
15949 &source_ref,
15950 AstMutateCommand::AddVariableDeclaration {
15951 prefix: "thing".to_owned(),
15952 },
15953 )
15954 .unwrap();
15955 let AstMutateCommandReturn::Name(name) = cmd_return else {
15956 panic!("expected a generated name");
15957 };
15958 assert_eq!(name, "thing2");
15959 let body = &function_body_at(&ast, 0).body;
15960 assert_eq!(body.len(), 3);
15961 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
15962 panic!("expected the expression statement to become a variable declaration");
15963 };
15964 assert_eq!(decl.name(), "thing2");
15965 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15967 panic!("expected a variable declaration");
15968 };
15969 assert_eq!(first.name(), "thing1");
15970 assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
15971 }
15972
15973 #[test]
15974 fn test_delete_node_in_function_body_preserves_leading_comment() {
15975 let code = "\
15979fn build() {
15980 a = 1
15981 // keep me
15982 b = 2
15983 return a
15984}
15985";
15986 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15987 let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
15988 panic!("expected a variable declaration");
15989 };
15990 assert_eq!(b_decl.name(), "b");
15991 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
15992 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
15993 let body = &function_body_at(&ast, 0).body;
15994 assert_eq!(body.len(), 2, "expected b to be deleted");
15995 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15996 panic!("expected a variable declaration");
15997 };
15998 assert_eq!(first.name(), "a");
15999 let ast::BodyItem::ReturnStatement(_) = &body[1] else {
16000 panic!("expected the return statement to remain");
16001 };
16002 assert!(
16003 body[1].get_comments().iter().any(|c| c.contains("keep me")),
16004 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16005 body[1].get_comments()
16006 );
16007 }
16008
16009 #[test]
16010 fn test_add_variable_declaration_in_function_body_ignores_parameters() {
16011 let code = "\
16019fn build(thing2) {
16020 thing1 = 1
16021 10 + 20
16022 return thing1 + thing2
16023}
16024";
16025 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16026 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16027 panic!("expected an expression statement");
16028 };
16029 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16030 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16031 &mut ast,
16032 &source_ref,
16033 AstMutateCommand::AddVariableDeclaration {
16034 prefix: "thing".to_owned(),
16035 },
16036 )
16037 .unwrap();
16038 let AstMutateCommandReturn::Name(name) = cmd_return else {
16039 panic!("expected a generated name");
16040 };
16041 assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16042 }
16043
16044 #[test]
16045 fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16046 let code = "\
16047x = 1
16048y = if x > 0 {
16049 q1 = 1
16050 foo(q1)
16051 q1
16052} else {
16053 2
16054}
16055";
16056 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16057 let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16058 panic!("expected an expression statement");
16059 };
16060 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16061 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16062 &mut ast,
16063 &source_ref,
16064 AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16065 )
16066 .unwrap();
16067 let AstMutateCommandReturn::Name(name) = cmd_return else {
16068 panic!("expected a generated name");
16069 };
16070 assert_eq!(name, "q2");
16071 let body = &then_block_at(&ast, 1).body;
16072 assert_eq!(body.len(), 3);
16073 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16074 panic!("expected the expression statement to become a variable declaration");
16075 };
16076 assert_eq!(decl.name(), "q2");
16077 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16079 panic!("expected a variable declaration");
16080 };
16081 assert_eq!(first.name(), "q1");
16082 assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16083 }
16084
16085 #[test]
16086 fn test_delete_node_in_if_branch_preserves_leading_comment() {
16087 let code = "\
16090y = if true {
16091 a = 1
16092 // keep me
16093 b = 2
16094 a + b
16095} else {
16096 2
16097}
16098";
16099 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16100 let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16101 panic!("expected a variable declaration");
16102 };
16103 assert_eq!(b_decl.name(), "b");
16104 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16105 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16106 let body = &then_block_at(&ast, 0).body;
16107 assert_eq!(body.len(), 2, "expected b to be deleted");
16108 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16109 panic!("expected a variable declaration");
16110 };
16111 assert_eq!(first.name(), "a");
16112 let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16113 panic!("expected the tail expression to remain");
16114 };
16115 assert!(
16116 body[1].get_comments().iter().any(|c| c.contains("keep me")),
16117 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16118 body[1].get_comments()
16119 );
16120 }
16121}