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 hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1965 self.program = program.clone();
1966
1967 self.point_freedom_cache.clear();
1978 match ctx.run_with_caching(program).await {
1979 Ok(outcome) => {
1980 let outcome = self.update_state_after_exec(outcome, true);
1981 let checkpoint_id = self
1982 .create_sketch_checkpoint(outcome.clone())
1983 .await
1984 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1985 Ok(SetProgramOutcome::Success {
1986 scene_graph: Box::new(self.scene_graph_for_ui()),
1987 exec_outcome: Box::new(outcome),
1988 checkpoint_id: Some(checkpoint_id),
1989 })
1990 }
1991 Err(mut err) => {
1992 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1995 self.update_state_after_exec(outcome, true);
1996 err.scene_graph = Some(self.scene_graph_for_ui());
1997 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1998 }
1999 }
2000 }
2001
2002 pub async fn engine_execute(
2005 &mut self,
2006 ctx: &ExecutorContext,
2007 program: Program,
2008 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
2009 self.program = program.clone();
2010
2011 self.point_freedom_cache.clear();
2015 match ctx.run_with_caching(program).await {
2016 Ok(outcome) => {
2017 let outcome = self.update_state_after_exec(outcome, true);
2018 Ok(SceneGraphDelta {
2019 new_graph: self.scene_graph_for_ui(),
2020 exec_outcome: outcome,
2021 new_objects: Default::default(),
2023 invalidates_ids: Default::default(),
2025 })
2026 }
2027 Err(mut err) => {
2028 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2030 self.update_state_after_exec(outcome, true);
2031 err.scene_graph = Some(self.scene_graph_for_ui());
2032 Err(err)
2033 }
2034 }
2035 }
2036
2037 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
2038 if matches!(err.error, KclError::EngineHangup { .. }) {
2039 return Err(err);
2043 }
2044
2045 let KclErrorWithOutputs {
2046 error,
2047 mut non_fatal,
2048 variables,
2049 operations,
2050 artifact_graph,
2051 scene_objects,
2052 source_range_to_object,
2053 var_solutions,
2054 refactor_metadata,
2055 filenames,
2056 source_files,
2057 default_planes,
2058 ..
2059 } = err;
2060
2061 non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2062
2063 Ok(ExecOutcome {
2064 variables,
2065 #[cfg(test)]
2066 test_program_memory: Default::default(),
2067 filenames,
2068 operations,
2069 artifact_graph,
2070 scene_objects,
2071 source_range_to_object,
2072 var_solutions,
2073 refactor_metadata,
2074 issues: non_fatal,
2075 source_files,
2076 default_planes,
2077 })
2078 }
2079
2080 async fn add_point(
2081 &mut self,
2082 ctx: &ExecutorContext,
2083 sketch: ObjectId,
2084 ctor: PointCtor,
2085 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2086 let at_ast = to_ast_point2d(&ctor.position)
2088 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2089 let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2090 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2091 unlabeled: None,
2092 arguments: vec![ast::LabeledArg {
2093 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2094 arg: at_ast,
2095 }],
2096 digest: None,
2097 non_code_meta: Default::default(),
2098 })));
2099
2100 let sketch_id = sketch;
2102 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2103 #[cfg(target_arch = "wasm32")]
2104 web_sys::console::error_1(
2105 &format!(
2106 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2107 self.scene_graph.objects
2108 )
2109 .into(),
2110 );
2111 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2112 })?;
2113 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2114 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2115 "Object is not a sketch, it is {}",
2116 sketch_object.kind.human_friendly_kind_with_article(),
2117 ))));
2118 };
2119 let mut new_ast = self.program.ast.clone();
2121 let (sketch_block_ref, _) = self
2122 .mutate_ast(
2123 &mut new_ast,
2124 sketch_id,
2125 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2126 )
2127 .map_err(KclErrorWithOutputs::no_outputs)?;
2128 let new_source = source_from_ast(&new_ast);
2130 let new_program = parse_frontend_mutation_source(
2132 &new_source,
2133 "Error parsing KCL source after adding point",
2134 "No AST produced after adding point",
2135 )?;
2136
2137 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2138 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2139 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2140 )))
2141 })?;
2142
2143 self.program = new_program.clone();
2145
2146 let mut truncated_program = new_program;
2148 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2149 .map_err(KclErrorWithOutputs::no_outputs)?;
2150
2151 let outcome = ctx
2153 .run_mock(
2154 &truncated_program,
2155 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2156 )
2157 .await?;
2158
2159 let new_object_ids = {
2160 let make_err =
2161 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2162 let segment_id = outcome
2163 .source_range_to_object
2164 .get(&point_node_ref.range)
2165 .copied()
2166 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2167 let segment_object = outcome
2168 .scene_objects
2169 .get(segment_id.0)
2170 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2171 let ObjectKind::Segment { segment } = &segment_object.kind else {
2172 return Err(make_err(format!(
2173 "Object is not a segment, it is {}",
2174 segment_object.kind.human_friendly_kind_with_article()
2175 )));
2176 };
2177 let Segment::Point(_) = segment else {
2178 return Err(make_err(format!(
2179 "Segment is not a point, it is {}",
2180 segment.human_friendly_kind_with_article()
2181 )));
2182 };
2183 vec![segment_id]
2184 };
2185 let src_delta = SourceDelta { text: new_source };
2186 let outcome = self.update_state_after_exec(outcome, false);
2188 let scene_graph_delta = SceneGraphDelta {
2189 new_graph: self.scene_graph_for_ui(),
2190 invalidates_ids: false,
2191 new_objects: new_object_ids,
2192 exec_outcome: outcome,
2193 };
2194 Ok((src_delta, scene_graph_delta))
2195 }
2196
2197 async fn add_line(
2198 &mut self,
2199 ctx: &ExecutorContext,
2200 sketch: ObjectId,
2201 ctor: LineCtor,
2202 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2203 let start_ast = to_ast_point2d(&ctor.start)
2205 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2206 let end_ast = to_ast_point2d(&ctor.end)
2207 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2208 let mut arguments = vec![
2209 ast::LabeledArg {
2210 label: Some(ast::Identifier::new(LINE_START_PARAM)),
2211 arg: start_ast,
2212 },
2213 ast::LabeledArg {
2214 label: Some(ast::Identifier::new(LINE_END_PARAM)),
2215 arg: end_ast,
2216 },
2217 ];
2218 if ctor.construction == Some(true) {
2220 arguments.push(ast::LabeledArg {
2221 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2222 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2223 value: ast::LiteralValue::Bool(true),
2224 raw: "true".to_string(),
2225 digest: None,
2226 }))),
2227 });
2228 }
2229 let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2230 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2231 unlabeled: None,
2232 arguments,
2233 digest: None,
2234 non_code_meta: Default::default(),
2235 })));
2236
2237 let sketch_id = sketch;
2239 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2240 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2241 })?;
2242 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2243 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2244 "Object is not a sketch, it is {}",
2245 sketch_object.kind.human_friendly_kind_with_article(),
2246 ))));
2247 };
2248 let mut new_ast = self.program.ast.clone();
2250 let (sketch_block_ref, _) = self
2251 .mutate_ast(
2252 &mut new_ast,
2253 sketch_id,
2254 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2255 )
2256 .map_err(KclErrorWithOutputs::no_outputs)?;
2257 let new_source = source_from_ast(&new_ast);
2259 let new_program = parse_frontend_mutation_source(
2261 &new_source,
2262 "Error parsing KCL source after adding line",
2263 "No AST produced after adding line",
2264 )?;
2265
2266 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2267 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2268 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2269 )))
2270 })?;
2271
2272 self.program = new_program.clone();
2274
2275 let mut truncated_program = new_program;
2277 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2278 .map_err(KclErrorWithOutputs::no_outputs)?;
2279
2280 let outcome = ctx
2282 .run_mock(
2283 &truncated_program,
2284 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2285 )
2286 .await?;
2287
2288 let new_object_ids = {
2289 let make_err =
2290 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2291 let segment_id = outcome
2292 .source_range_to_object
2293 .get(&line_node_ref.range)
2294 .copied()
2295 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2296 let segment_object = outcome
2297 .scene_object_by_id(segment_id)
2298 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2299 let ObjectKind::Segment { segment } = &segment_object.kind else {
2300 return Err(make_err(format!(
2301 "Object is not a segment, it is {}",
2302 segment_object.kind.human_friendly_kind_with_article()
2303 )));
2304 };
2305 let Segment::Line(line) = segment else {
2306 return Err(make_err(format!(
2307 "Segment is not a line, it is {}",
2308 segment.human_friendly_kind_with_article()
2309 )));
2310 };
2311 vec![line.start, line.end, segment_id]
2312 };
2313 let src_delta = SourceDelta { text: new_source };
2314 let outcome = self.update_state_after_exec(outcome, false);
2316 let scene_graph_delta = SceneGraphDelta {
2317 new_graph: self.scene_graph_for_ui(),
2318 invalidates_ids: false,
2319 new_objects: new_object_ids,
2320 exec_outcome: outcome,
2321 };
2322 Ok((src_delta, scene_graph_delta))
2323 }
2324
2325 async fn add_arc(
2326 &mut self,
2327 ctx: &ExecutorContext,
2328 sketch: ObjectId,
2329 ctor: ArcCtor,
2330 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2331 let start_ast = to_ast_point2d(&ctor.start)
2333 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2334 let end_ast = to_ast_point2d(&ctor.end)
2335 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2336 let center_ast = to_ast_point2d(&ctor.center)
2337 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2338 let mut arguments = vec![
2339 ast::LabeledArg {
2340 label: Some(ast::Identifier::new(ARC_START_PARAM)),
2341 arg: start_ast,
2342 },
2343 ast::LabeledArg {
2344 label: Some(ast::Identifier::new(ARC_END_PARAM)),
2345 arg: end_ast,
2346 },
2347 ast::LabeledArg {
2348 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2349 arg: center_ast,
2350 },
2351 ];
2352 if ctor.direction == Some(ArcDirection::Cw) {
2355 arguments.push(ast::LabeledArg {
2356 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2357 arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2358 });
2359 }
2360 if ctor.construction == Some(true) {
2362 arguments.push(ast::LabeledArg {
2363 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2364 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2365 value: ast::LiteralValue::Bool(true),
2366 raw: "true".to_string(),
2367 digest: None,
2368 }))),
2369 });
2370 }
2371 let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2372 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2373 unlabeled: None,
2374 arguments,
2375 digest: None,
2376 non_code_meta: Default::default(),
2377 })));
2378
2379 let sketch_id = sketch;
2381 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2382 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2383 })?;
2384 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2385 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2386 "Object is not a sketch, it is {}",
2387 sketch_object.kind.human_friendly_kind_with_article(),
2388 ))));
2389 };
2390 let mut new_ast = self.program.ast.clone();
2392 let (sketch_block_ref, _) = self
2393 .mutate_ast(
2394 &mut new_ast,
2395 sketch_id,
2396 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2397 )
2398 .map_err(KclErrorWithOutputs::no_outputs)?;
2399 let new_source = source_from_ast(&new_ast);
2401 let new_program = parse_frontend_mutation_source(
2403 &new_source,
2404 "Error parsing KCL source after adding arc",
2405 "No AST produced after adding arc",
2406 )?;
2407
2408 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2409 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2410 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2411 )))
2412 })?;
2413
2414 self.program = new_program.clone();
2416
2417 let mut truncated_program = new_program;
2419 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2420 .map_err(KclErrorWithOutputs::no_outputs)?;
2421
2422 let outcome = ctx
2424 .run_mock(
2425 &truncated_program,
2426 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2427 )
2428 .await?;
2429
2430 let new_object_ids = {
2431 let make_err =
2432 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2433 let segment_id = outcome
2434 .source_range_to_object
2435 .get(&arc_node_ref.range)
2436 .copied()
2437 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2438 let segment_object = outcome
2439 .scene_objects
2440 .get(segment_id.0)
2441 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2442 let ObjectKind::Segment { segment } = &segment_object.kind else {
2443 return Err(make_err(format!(
2444 "Object is not a segment, it is {}",
2445 segment_object.kind.human_friendly_kind_with_article()
2446 )));
2447 };
2448 let Segment::Arc(arc) = segment else {
2449 return Err(make_err(format!(
2450 "Segment is not an arc, it is {}",
2451 segment.human_friendly_kind_with_article()
2452 )));
2453 };
2454 vec![arc.start, arc.end, arc.center, segment_id]
2455 };
2456 let src_delta = SourceDelta { text: new_source };
2457 let outcome = self.update_state_after_exec(outcome, false);
2459 let scene_graph_delta = SceneGraphDelta {
2460 new_graph: self.scene_graph_for_ui(),
2461 invalidates_ids: false,
2462 new_objects: new_object_ids,
2463 exec_outcome: outcome,
2464 };
2465 Ok((src_delta, scene_graph_delta))
2466 }
2467
2468 async fn add_circle(
2469 &mut self,
2470 ctx: &ExecutorContext,
2471 sketch: ObjectId,
2472 ctor: CircleCtor,
2473 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2474 let start_ast = to_ast_point2d(&ctor.start)
2476 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2477 let center_ast = to_ast_point2d(&ctor.center)
2478 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2479 let mut arguments = vec![
2480 ast::LabeledArg {
2481 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2482 arg: start_ast,
2483 },
2484 ast::LabeledArg {
2485 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2486 arg: center_ast,
2487 },
2488 ];
2489 if ctor.construction == Some(true) {
2491 arguments.push(ast::LabeledArg {
2492 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2493 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2494 value: ast::LiteralValue::Bool(true),
2495 raw: "true".to_string(),
2496 digest: None,
2497 }))),
2498 });
2499 }
2500 let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2501 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2502 unlabeled: None,
2503 arguments,
2504 digest: None,
2505 non_code_meta: Default::default(),
2506 })));
2507
2508 let sketch_id = sketch;
2510 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2511 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2512 })?;
2513 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2514 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2515 "Object is not a sketch, it is {}",
2516 sketch_object.kind.human_friendly_kind_with_article(),
2517 ))));
2518 };
2519 let mut new_ast = self.program.ast.clone();
2521 let (sketch_block_ref, _) = self
2522 .mutate_ast(
2523 &mut new_ast,
2524 sketch_id,
2525 AstMutateCommand::AddSketchBlockVarDecl {
2526 prefix: CIRCLE_VARIABLE.to_owned(),
2527 expr: circle_ast,
2528 },
2529 )
2530 .map_err(KclErrorWithOutputs::no_outputs)?;
2531 let new_source = source_from_ast(&new_ast);
2533 let new_program = parse_frontend_mutation_source(
2535 &new_source,
2536 "Error parsing KCL source after adding circle",
2537 "No AST produced after adding circle",
2538 )?;
2539
2540 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2541 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2542 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2543 )))
2544 })?;
2545
2546 self.program = new_program.clone();
2548
2549 let mut truncated_program = new_program;
2551 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2552 .map_err(KclErrorWithOutputs::no_outputs)?;
2553
2554 let outcome = ctx
2556 .run_mock(
2557 &truncated_program,
2558 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2559 )
2560 .await?;
2561
2562 let new_object_ids = {
2563 let make_err =
2564 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2565 let segment_id = outcome
2566 .source_range_to_object
2567 .get(&circle_node_ref.range)
2568 .copied()
2569 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2570 let segment_object = outcome
2571 .scene_objects
2572 .get(segment_id.0)
2573 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2574 let ObjectKind::Segment { segment } = &segment_object.kind else {
2575 return Err(make_err(format!(
2576 "Object is not a segment, it is {}",
2577 segment_object.kind.human_friendly_kind_with_article()
2578 )));
2579 };
2580 let Segment::Circle(circle) = segment else {
2581 return Err(make_err(format!(
2582 "Segment is not a circle, it is {}",
2583 segment.human_friendly_kind_with_article()
2584 )));
2585 };
2586 vec![circle.start, circle.center, segment_id]
2587 };
2588 let src_delta = SourceDelta { text: new_source };
2589 let outcome = self.update_state_after_exec(outcome, false);
2591 let scene_graph_delta = SceneGraphDelta {
2592 new_graph: self.scene_graph_for_ui(),
2593 invalidates_ids: false,
2594 new_objects: new_object_ids,
2595 exec_outcome: outcome,
2596 };
2597 Ok((src_delta, scene_graph_delta))
2598 }
2599
2600 async fn add_control_point_spline(
2601 &mut self,
2602 ctx: &ExecutorContext,
2603 sketch: ObjectId,
2604 ctor: ControlPointSplineCtor,
2605 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2606 let new_program = ensure_control_point_spline_experimental_features(&self.program)
2607 .map_err(KclErrorWithOutputs::no_outputs)?;
2608
2609 let points_ast = to_ast_point2d_array(&ctor.points)
2610 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2611 let mut arguments = vec![ast::LabeledArg {
2612 label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2613 arg: points_ast,
2614 }];
2615 if ctor.construction == Some(true) {
2616 arguments.push(ast::LabeledArg {
2617 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2618 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2619 value: ast::LiteralValue::Bool(true),
2620 raw: "true".to_string(),
2621 digest: None,
2622 }))),
2623 });
2624 }
2625 let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2626 callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2627 unlabeled: None,
2628 arguments,
2629 digest: None,
2630 non_code_meta: Default::default(),
2631 })));
2632
2633 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2634 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2635 })?;
2636 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2637 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2638 "Object is not a sketch, it is {}",
2639 sketch_object.kind.human_friendly_kind_with_article(),
2640 ))));
2641 };
2642
2643 let mut new_ast = new_program.ast.clone();
2644 let (sketch_block_ref, _) = self
2645 .mutate_ast(
2646 &mut new_ast,
2647 sketch,
2648 AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2649 )
2650 .map_err(KclErrorWithOutputs::no_outputs)?;
2651 let new_source = source_from_ast(&new_ast);
2652 let new_program = parse_frontend_mutation_source(
2653 &new_source,
2654 "Error parsing KCL source after adding controlPointSpline",
2655 "No AST produced after adding controlPointSpline",
2656 )?;
2657
2658 let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2659 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2660 "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2661 )))
2662 })?;
2663
2664 self.program = new_program.clone();
2665
2666 let mut truncated_program = new_program;
2667 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2668 .map_err(KclErrorWithOutputs::no_outputs)?;
2669
2670 let outcome = ctx
2671 .run_mock(
2672 &truncated_program,
2673 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2674 )
2675 .await?;
2676
2677 let new_object_ids = {
2678 let make_err =
2679 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2680 let segment_id = outcome
2681 .source_range_to_object
2682 .get(&spline_node_ref.range)
2683 .copied()
2684 .ok_or_else(|| {
2685 make_err(format!(
2686 "Source range of controlPointSpline not found: {spline_node_ref:?}"
2687 ))
2688 })?;
2689 let segment_object = outcome
2690 .scene_objects
2691 .get(segment_id.0)
2692 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2693 let ObjectKind::Segment { segment } = &segment_object.kind else {
2694 return Err(make_err(format!(
2695 "Object is not a segment, it is {}",
2696 segment_object.kind.human_friendly_kind_with_article()
2697 )));
2698 };
2699 let Segment::ControlPointSpline(spline) = segment else {
2700 return Err(make_err(format!(
2701 "Segment is not a control point spline, it is {}",
2702 segment.human_friendly_kind_with_article()
2703 )));
2704 };
2705
2706 let mut ids = outcome
2707 .scene_objects
2708 .iter()
2709 .filter_map(|obj| match &obj.kind {
2710 ObjectKind::Segment {
2711 segment: Segment::Line(line),
2712 } if line.owner == Some(segment_id) => Some(obj.id),
2713 _ => None,
2714 })
2715 .collect::<Vec<_>>();
2716 ids.extend(spline.controls.clone());
2717 ids.push(segment_id);
2718 ids
2719 };
2720 let src_delta = SourceDelta { text: new_source };
2721 let outcome = self.update_state_after_exec(outcome, false);
2722 let scene_graph_delta = SceneGraphDelta {
2723 new_graph: self.scene_graph_for_ui(),
2724 invalidates_ids: false,
2725 new_objects: new_object_ids,
2726 exec_outcome: outcome,
2727 };
2728 Ok((src_delta, scene_graph_delta))
2729 }
2730
2731 fn edit_point(
2732 &mut self,
2733 new_ast: &mut ast::Node<ast::Program>,
2734 sketch: ObjectId,
2735 point: ObjectId,
2736 ctor: PointCtor,
2737 ) -> Result<(), KclError> {
2738 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2740
2741 let sketch_id = sketch;
2743 let sketch_object = self
2744 .scene_graph
2745 .objects
2746 .get(sketch_id.0)
2747 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2748 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2749 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2750 };
2751 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2752 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2753 })?;
2754 let point_id = point;
2756 let point_object = self
2757 .scene_graph
2758 .objects
2759 .get(point_id.0)
2760 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2761 let ObjectKind::Segment {
2762 segment: Segment::Point(point),
2763 } = &point_object.kind
2764 else {
2765 return Err(KclError::refactor(format!(
2766 "Object is not a point segment: {point_object:?}"
2767 )));
2768 };
2769
2770 if let Some(owner_id) = point.owner {
2772 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2773 KclError::refactor(format!(
2774 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2775 ))
2776 })?;
2777 let ObjectKind::Segment { segment } = &owner_object.kind else {
2778 return Err(KclError::refactor(format!(
2779 "Internal: Owner of point is not a segment, but found {}",
2780 owner_object.kind.human_friendly_kind_with_article()
2781 )));
2782 };
2783
2784 if let Segment::Line(line) = segment {
2786 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2787 return Err(KclError::refactor(format!(
2788 "Internal: Owner of point does not have line ctor, but found {}",
2789 line.ctor.human_friendly_kind_with_article()
2790 )));
2791 };
2792 let mut line_ctor = line_ctor.clone();
2793 if line.start == point_id {
2795 line_ctor.start = ctor.position;
2796 } else if line.end == point_id {
2797 line_ctor.end = ctor.position;
2798 } else {
2799 return Err(KclError::refactor(format!(
2800 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2801 )));
2802 }
2803 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2804 }
2805
2806 if let Segment::Arc(arc) = segment {
2808 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2809 return Err(KclError::refactor(format!(
2810 "Internal: Owner of point does not have arc ctor, but found {}",
2811 arc.ctor.human_friendly_kind_with_article()
2812 )));
2813 };
2814 let mut arc_ctor = arc_ctor.clone();
2815 if arc.center == point_id {
2817 arc_ctor.center = ctor.position;
2818 } else if arc.start == point_id {
2819 arc_ctor.start = ctor.position;
2820 } else if arc.end == point_id {
2821 arc_ctor.end = ctor.position;
2822 } else {
2823 return Err(KclError::refactor(format!(
2824 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2825 )));
2826 }
2827 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2828 }
2829
2830 if let Segment::Circle(circle) = segment {
2832 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2833 return Err(KclError::refactor(format!(
2834 "Internal: Owner of point does not have circle ctor, but found {}",
2835 circle.ctor.human_friendly_kind_with_article()
2836 )));
2837 };
2838 let mut circle_ctor = circle_ctor.clone();
2839 if circle.center == point_id {
2840 circle_ctor.center = ctor.position;
2841 } else if circle.start == point_id {
2842 circle_ctor.start = ctor.position;
2843 } else {
2844 return Err(KclError::refactor(format!(
2845 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2846 )));
2847 }
2848 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2849 }
2850
2851 if let Segment::ControlPointSpline(spline) = segment {
2852 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2853 return Err(KclError::refactor(format!(
2854 "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2855 spline.ctor.human_friendly_kind_with_article()
2856 )));
2857 };
2858 let mut spline_ctor = spline_ctor.clone();
2859 let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2860 return Err(KclError::refactor(format!(
2861 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2862 )));
2863 };
2864 spline_ctor.points[control_index] = ctor.position;
2865 return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2866 }
2867
2868 }
2871
2872 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2874 Ok(())
2875 }
2876
2877 fn edit_line(
2878 &mut self,
2879 new_ast: &mut ast::Node<ast::Program>,
2880 sketch: ObjectId,
2881 line: ObjectId,
2882 ctor: LineCtor,
2883 ) -> Result<(), KclError> {
2884 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2886 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2887
2888 let sketch_id = sketch;
2890 let sketch_object = self
2891 .scene_graph
2892 .objects
2893 .get(sketch_id.0)
2894 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2895 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2896 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2897 };
2898 sketch
2899 .segments
2900 .iter()
2901 .find(|o| **o == line)
2902 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2903 let line_id = line;
2905 let line_object = self
2906 .scene_graph
2907 .objects
2908 .get(line_id.0)
2909 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2910 let ObjectKind::Segment { .. } = &line_object.kind else {
2911 let kind = line_object.kind.human_friendly_kind_with_article();
2912 return Err(KclError::refactor(format!(
2913 "This constraint only works on Segments, but you selected {kind}"
2914 )));
2915 };
2916
2917 self.mutate_ast(
2919 new_ast,
2920 line_id,
2921 AstMutateCommand::EditLine {
2922 start: new_start_ast,
2923 end: new_end_ast,
2924 construction: ctor.construction,
2925 },
2926 )?;
2927 Ok(())
2928 }
2929
2930 fn edit_arc(
2931 &mut self,
2932 new_ast: &mut ast::Node<ast::Program>,
2933 sketch: ObjectId,
2934 arc: ObjectId,
2935 ctor: ArcCtor,
2936 ) -> Result<(), KclError> {
2937 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2939 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2940 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2941
2942 let sketch_id = sketch;
2944 let sketch_object = self
2945 .scene_graph
2946 .objects
2947 .get(sketch_id.0)
2948 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2949 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2950 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2951 };
2952 sketch
2953 .segments
2954 .iter()
2955 .find(|o| **o == arc)
2956 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2957 let arc_id = arc;
2959 let arc_object = self
2960 .scene_graph
2961 .objects
2962 .get(arc_id.0)
2963 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2964 let ObjectKind::Segment { .. } = &arc_object.kind else {
2965 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2966 };
2967
2968 self.mutate_ast(
2970 new_ast,
2971 arc_id,
2972 AstMutateCommand::EditArc {
2973 start: new_start_ast,
2974 end: new_end_ast,
2975 center: new_center_ast,
2976 direction: ctor.direction,
2977 construction: ctor.construction,
2978 },
2979 )?;
2980 Ok(())
2981 }
2982
2983 fn edit_circle(
2984 &mut self,
2985 new_ast: &mut ast::Node<ast::Program>,
2986 sketch: ObjectId,
2987 circle: ObjectId,
2988 ctor: CircleCtor,
2989 ) -> Result<(), KclError> {
2990 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2992 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2993
2994 let sketch_id = sketch;
2996 let sketch_object = self
2997 .scene_graph
2998 .objects
2999 .get(sketch_id.0)
3000 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3001 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3002 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3003 };
3004 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3005 KclError::refactor(format!(
3006 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3007 ))
3008 })?;
3009 let circle_id = circle;
3011 let circle_object = self
3012 .scene_graph
3013 .objects
3014 .get(circle_id.0)
3015 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3016 let ObjectKind::Segment { .. } = &circle_object.kind else {
3017 return Err(KclError::refactor(format!(
3018 "Object is not a segment: {circle_object:?}"
3019 )));
3020 };
3021
3022 self.mutate_ast(
3024 new_ast,
3025 circle_id,
3026 AstMutateCommand::EditCircle {
3027 start: new_start_ast,
3028 center: new_center_ast,
3029 construction: ctor.construction,
3030 },
3031 )?;
3032 Ok(())
3033 }
3034
3035 fn edit_control_point_spline(
3036 &mut self,
3037 new_ast: &mut ast::Node<ast::Program>,
3038 sketch: ObjectId,
3039 spline: ObjectId,
3040 ctor: ControlPointSplineCtor,
3041 ) -> Result<(), KclError> {
3042 let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3043
3044 let sketch_object = self
3045 .scene_graph
3046 .objects
3047 .get(sketch.0)
3048 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3049 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3050 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3051 };
3052 sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3053 KclError::refactor(format!(
3054 "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3055 ))
3056 })?;
3057
3058 let spline_object =
3059 self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3060 KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3061 })?;
3062 let ObjectKind::Segment { .. } = &spline_object.kind else {
3063 return Err(KclError::refactor(format!(
3064 "Object is not a segment: {spline_object:?}"
3065 )));
3066 };
3067
3068 self.mutate_ast(
3069 new_ast,
3070 spline,
3071 AstMutateCommand::EditControlPointSpline {
3072 points: points_ast,
3073 construction: ctor.construction,
3074 },
3075 )?;
3076 Ok(())
3077 }
3078
3079 fn delete_segment(
3080 &mut self,
3081 new_ast: &mut ast::Node<ast::Program>,
3082 sketch: ObjectId,
3083 segment_id: ObjectId,
3084 ) -> Result<(), KclError> {
3085 let sketch_id = sketch;
3087 let sketch_object = self
3088 .scene_graph
3089 .objects
3090 .get(sketch_id.0)
3091 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3092 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3093 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3094 };
3095 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3096 KclError::refactor(format!(
3097 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3098 ))
3099 })?;
3100 let segment_object =
3102 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3103 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3104 })?;
3105 let ObjectKind::Segment { .. } = &segment_object.kind else {
3106 return Err(KclError::refactor(format!(
3107 "Object is not a segment, it is {}",
3108 segment_object.kind.human_friendly_kind_with_article()
3109 )));
3110 };
3111
3112 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3114 Ok(())
3115 }
3116
3117 fn delete_constraint(
3118 &mut self,
3119 new_ast: &mut ast::Node<ast::Program>,
3120 sketch: ObjectId,
3121 constraint_id: ObjectId,
3122 ) -> Result<(), KclError> {
3123 let sketch_id = sketch;
3125 let sketch_object = self
3126 .scene_graph
3127 .objects
3128 .get(sketch_id.0)
3129 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3130 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3131 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3132 };
3133 sketch
3134 .constraints
3135 .iter()
3136 .find(|o| **o == constraint_id)
3137 .ok_or_else(|| {
3138 KclError::refactor(format!(
3139 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3140 ))
3141 })?;
3142 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3144 KclError::refactor(format!(
3145 "Constraint not found in scene graph: constraint={constraint_id:?}"
3146 ))
3147 })?;
3148 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3149 return Err(KclError::refactor(format!(
3150 "Object is not a constraint, it is {}",
3151 constraint_object.kind.human_friendly_kind_with_article()
3152 )));
3153 };
3154
3155 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3157 Ok(())
3158 }
3159
3160 fn edit_coincident_constraint(
3161 &mut self,
3162 new_ast: &mut ast::Node<ast::Program>,
3163 constraint_id: ObjectId,
3164 segments: Vec<ConstraintSegment>,
3165 ) -> Result<(), KclError> {
3166 if segments.len() < 2 {
3167 return Err(KclError::refactor(format!(
3168 "Coincident constraint must have at least 2 inputs, got {}",
3169 segments.len()
3170 )));
3171 }
3172
3173 let segment_asts = segments
3174 .iter()
3175 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3176 .collect::<Result<Vec<_>, _>>()?;
3177
3178 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3179 elements: segment_asts,
3180 digest: None,
3181 non_code_meta: Default::default(),
3182 })));
3183
3184 self.mutate_ast(
3185 new_ast,
3186 constraint_id,
3187 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3188 )?;
3189 Ok(())
3190 }
3191
3192 fn edit_horizontal_points_constraint(
3193 &mut self,
3194 new_ast: &mut ast::Node<ast::Program>,
3195 constraint_id: ObjectId,
3196 points: Vec<ConstraintSegment>,
3197 ) -> Result<(), KclError> {
3198 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3199 }
3200
3201 fn edit_vertical_points_constraint(
3202 &mut self,
3203 new_ast: &mut ast::Node<ast::Program>,
3204 constraint_id: ObjectId,
3205 points: Vec<ConstraintSegment>,
3206 ) -> Result<(), KclError> {
3207 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3208 }
3209
3210 fn edit_axis_points_constraint(
3211 &mut self,
3212 new_ast: &mut ast::Node<ast::Program>,
3213 constraint_id: ObjectId,
3214 points: Vec<ConstraintSegment>,
3215 constraint_name: &str,
3216 ) -> Result<(), KclError> {
3217 if points.len() < 2 {
3218 return Err(KclError::refactor(format!(
3219 "{constraint_name} points constraint must have at least 2 points, got {}",
3220 points.len()
3221 )));
3222 }
3223
3224 let point_asts = points
3225 .iter()
3226 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3227 .collect::<Result<Vec<_>, _>>()?;
3228
3229 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3230 elements: point_asts,
3231 digest: None,
3232 non_code_meta: Default::default(),
3233 })));
3234
3235 self.mutate_ast(
3236 new_ast,
3237 constraint_id,
3238 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3239 )?;
3240 Ok(())
3241 }
3242
3243 fn edit_equal_length_constraint(
3245 &mut self,
3246 new_ast: &mut ast::Node<ast::Program>,
3247 constraint_id: ObjectId,
3248 lines: Vec<ObjectId>,
3249 ) -> Result<(), KclError> {
3250 if lines.len() < 2 {
3251 return Err(KclError::refactor(format!(
3252 "Lines equal length constraint must have at least 2 lines, got {}",
3253 lines.len()
3254 )));
3255 }
3256
3257 let line_asts = lines
3258 .iter()
3259 .map(|line_id| {
3260 let line_object = self
3261 .scene_graph
3262 .objects
3263 .get(line_id.0)
3264 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3265 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3266 let kind = line_object.kind.human_friendly_kind_with_article();
3267 return Err(KclError::refactor(format!(
3268 "This constraint only works on Segments, but you selected {kind}"
3269 )));
3270 };
3271 let Segment::Line(_) = line_segment else {
3272 let kind = line_segment.human_friendly_kind_with_article();
3273 return Err(KclError::refactor(format!(
3274 "Only lines can be made equal length, but you selected {kind}"
3275 )));
3276 };
3277
3278 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3279 })
3280 .collect::<Result<Vec<_>, _>>()?;
3281
3282 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3283 elements: line_asts,
3284 digest: None,
3285 non_code_meta: Default::default(),
3286 })));
3287
3288 self.mutate_ast(
3289 new_ast,
3290 constraint_id,
3291 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3292 )?;
3293 Ok(())
3294 }
3295
3296 fn edit_parallel_constraint(
3298 &mut self,
3299 new_ast: &mut ast::Node<ast::Program>,
3300 constraint_id: ObjectId,
3301 lines: Vec<ObjectId>,
3302 ) -> Result<(), KclError> {
3303 if lines.len() < 2 {
3304 return Err(KclError::refactor(format!(
3305 "Parallel constraint must have at least 2 lines, got {}",
3306 lines.len()
3307 )));
3308 }
3309
3310 let line_asts = lines
3311 .iter()
3312 .map(|line_id| {
3313 let line_object = self
3314 .scene_graph
3315 .objects
3316 .get(line_id.0)
3317 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3318 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3319 let kind = line_object.kind.human_friendly_kind_with_article();
3320 return Err(KclError::refactor(format!(
3321 "This constraint only works on Segments, but you selected {kind}"
3322 )));
3323 };
3324 let Segment::Line(_) = line_segment else {
3325 let kind = line_segment.human_friendly_kind_with_article();
3326 return Err(KclError::refactor(format!(
3327 "Only lines can be made parallel, but you selected {kind}"
3328 )));
3329 };
3330
3331 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3332 })
3333 .collect::<Result<Vec<_>, _>>()?;
3334
3335 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3336 elements: line_asts,
3337 digest: None,
3338 non_code_meta: Default::default(),
3339 })));
3340
3341 self.mutate_ast(
3342 new_ast,
3343 constraint_id,
3344 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3345 )?;
3346 Ok(())
3347 }
3348
3349 fn edit_equal_radius_constraint(
3351 &mut self,
3352 new_ast: &mut ast::Node<ast::Program>,
3353 constraint_id: ObjectId,
3354 input: Vec<ObjectId>,
3355 ) -> Result<(), KclError> {
3356 if input.len() < 2 {
3357 return Err(KclError::refactor(format!(
3358 "equalRadius constraint must have at least 2 segments, got {}",
3359 input.len()
3360 )));
3361 }
3362
3363 let input_asts = input
3364 .iter()
3365 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3366 .collect::<Result<Vec<_>, _>>()?;
3367
3368 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3369 elements: input_asts,
3370 digest: None,
3371 non_code_meta: Default::default(),
3372 })));
3373
3374 self.mutate_ast(
3375 new_ast,
3376 constraint_id,
3377 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3378 )?;
3379 Ok(())
3380 }
3381
3382 async fn execute_after_edit(
3383 &mut self,
3384 ctx: &ExecutorContext,
3385 sketch: ObjectId,
3386 sketch_block_ref: AstNodeRef,
3387 new_ast: &mut ast::Node<ast::Program>,
3388 options: ExecuteAfterEditOptions,
3389 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3390 let ExecuteAfterEditOptions {
3391 segment_ids_edited,
3392 edit_kind,
3393 commit_solved_initial_guesses,
3394 } = options;
3395
3396 let new_source = source_from_ast(new_ast);
3398 let new_program = parse_frontend_mutation_source(
3400 &new_source,
3401 "Error parsing KCL source after editing",
3402 "No AST produced after editing",
3403 )?;
3404
3405 let is_delete = edit_kind.is_delete();
3407 let truncated_program = {
3408 let mut truncated_program = new_program.clone();
3409 only_sketch_block(
3410 &mut truncated_program.ast,
3411 &sketch_block_ref,
3412 edit_kind.to_change_kind(),
3413 )
3414 .map_err(KclErrorWithOutputs::no_outputs)?;
3415 truncated_program
3416 };
3417
3418 let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3420 let mock_config = MockConfig {
3421 sketch_block_id: Some(sketch),
3422 freedom_analysis: is_delete,
3423 segment_ids_edited: segment_ids_edited.clone(),
3424 drag_anchors,
3425 ..Default::default()
3426 };
3427 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3428
3429 self.program = new_program;
3431
3432 let outcome = self.update_state_after_exec(outcome, is_delete);
3434
3435 let src_delta = if commit_solved_initial_guesses {
3436 self.commit_var_solutions_to_program(&outcome, "editing")?
3437 } else {
3438 SourceDelta { text: new_source }
3439 };
3440 let scene_graph_delta = SceneGraphDelta {
3441 new_graph: self.scene_graph_for_ui(),
3442 invalidates_ids: is_delete,
3443 new_objects: Vec::new(),
3444 exec_outcome: outcome,
3445 };
3446 Ok((src_delta, scene_graph_delta))
3447 }
3448
3449 async fn execute_after_delete_sketch(
3450 &mut self,
3451 ctx: &ExecutorContext,
3452 new_ast: &mut ast::Node<ast::Program>,
3453 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3454 let new_source = source_from_ast(new_ast);
3456 let new_program = parse_frontend_mutation_source(
3458 &new_source,
3459 "Error parsing KCL source after editing",
3460 "No AST produced after editing",
3461 )?;
3462
3463 self.program = new_program.clone();
3465
3466 let outcome = ctx.run_with_caching(new_program).await?;
3472 let freedom_analysis_ran = true;
3473
3474 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3475
3476 let src_delta = SourceDelta { text: new_source };
3477 let scene_graph_delta = SceneGraphDelta {
3478 new_graph: self.scene_graph_for_ui(),
3479 invalidates_ids: true,
3480 new_objects: Vec::new(),
3481 exec_outcome: outcome,
3482 };
3483 Ok((src_delta, scene_graph_delta))
3484 }
3485
3486 fn point_id_to_ast_reference(
3491 &self,
3492 point_id: ObjectId,
3493 new_ast: &mut ast::Node<ast::Program>,
3494 ) -> Result<ast::Expr, KclError> {
3495 let point_object = self
3496 .scene_graph
3497 .objects
3498 .get(point_id.0)
3499 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3500 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3501 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3502 };
3503 let Segment::Point(point) = point_segment else {
3504 return Err(KclError::refactor(format!(
3505 "Only points are currently supported: {point_object:?}"
3506 )));
3507 };
3508
3509 if let Some(owner_id) = point.owner {
3510 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3511 KclError::refactor(format!(
3512 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3513 ))
3514 })?;
3515 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3516 return Err(KclError::refactor(format!(
3517 "Owner of point is not a segment, but found {}",
3518 owner_object.kind.human_friendly_kind_with_article()
3519 )));
3520 };
3521
3522 match owner_segment {
3523 Segment::Line(line) => {
3524 let property = if line.start == point_id {
3525 LINE_PROPERTY_START
3526 } else if line.end == point_id {
3527 LINE_PROPERTY_END
3528 } else {
3529 return Err(KclError::refactor(format!(
3530 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3531 )));
3532 };
3533 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3534 }
3535 Segment::Arc(arc) => {
3536 let property = if arc.start == point_id {
3537 ARC_PROPERTY_START
3538 } else if arc.end == point_id {
3539 ARC_PROPERTY_END
3540 } else if arc.center == point_id {
3541 ARC_PROPERTY_CENTER
3542 } else {
3543 return Err(KclError::refactor(format!(
3544 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3545 )));
3546 };
3547 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3548 }
3549 Segment::Circle(circle) => {
3550 let property = if circle.start == point_id {
3551 CIRCLE_PROPERTY_START
3552 } else if circle.center == point_id {
3553 CIRCLE_PROPERTY_CENTER
3554 } else {
3555 return Err(KclError::refactor(format!(
3556 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3557 )));
3558 };
3559 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3560 }
3561 Segment::ControlPointSpline(spline) => {
3562 let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3563 return Err(KclError::refactor(format!(
3564 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3565 )));
3566 };
3567 let owner_expr =
3568 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3569 let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3570 Ok(create_index_expression(controls_expr, index))
3571 }
3572 _ => Err(KclError::refactor(format!(
3573 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3574 ))),
3575 }
3576 } else {
3577 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3579 }
3580 }
3581
3582 fn line_id_to_ast_reference(
3583 &self,
3584 line_id: ObjectId,
3585 new_ast: &mut ast::Node<ast::Program>,
3586 ) -> Result<ast::Expr, KclError> {
3587 let line_object = self
3588 .scene_graph
3589 .objects
3590 .get(line_id.0)
3591 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3592 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3593 return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3594 };
3595 let Segment::Line(line) = line_segment else {
3596 return Err(KclError::refactor(format!(
3597 "Only lines are currently supported: {line_object:?}"
3598 )));
3599 };
3600
3601 if let Some(owner_id) = line.owner {
3602 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3603 KclError::refactor(format!(
3604 "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3605 ))
3606 })?;
3607 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3608 return Err(KclError::refactor(format!(
3609 "Owner of line is not a segment, but found {}",
3610 owner_object.kind.human_friendly_kind_with_article()
3611 )));
3612 };
3613
3614 match owner_segment {
3615 Segment::ControlPointSpline(spline) => {
3616 let Some(index) = spline
3617 .controls
3618 .windows(2)
3619 .position(|window| window[0] == line.start && window[1] == line.end)
3620 else {
3621 return Err(KclError::refactor(format!(
3622 "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3623 )));
3624 };
3625 let owner_expr =
3626 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3627 let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3628 Ok(create_index_expression(edges_expr, index))
3629 }
3630 _ => Err(KclError::refactor(format!(
3631 "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3632 ))),
3633 }
3634 } else {
3635 get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3636 }
3637 }
3638
3639 fn coincident_segment_to_ast(
3640 &self,
3641 segment: &ConstraintSegment,
3642 new_ast: &mut ast::Node<ast::Program>,
3643 ) -> Result<ast::Expr, KclError> {
3644 match segment {
3645 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3646 ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3647 }
3648 }
3649
3650 fn segment_id_to_constraint_ast_reference(
3651 &self,
3652 segment_id: ObjectId,
3653 new_ast: &mut ast::Node<ast::Program>,
3654 ) -> Result<ast::Expr, KclError> {
3655 let segment_object = self
3656 .scene_graph
3657 .objects
3658 .get(segment_id.0)
3659 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3660 let ObjectKind::Segment { segment } = &segment_object.kind else {
3661 return Err(KclError::refactor(format!(
3662 "Object is not a segment, it is {}",
3663 segment_object.kind.human_friendly_kind_with_article()
3664 )));
3665 };
3666
3667 match segment {
3668 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3669 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3670 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3671 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3672 Segment::ControlPointSpline(_) => {
3673 get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3674 }
3675 }
3676 }
3677
3678 fn axis_constraint_segment_to_ast(
3679 &self,
3680 segment: &ConstraintSegment,
3681 new_ast: &mut ast::Node<ast::Program>,
3682 ) -> Result<ast::Expr, KclError> {
3683 match segment {
3684 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3685 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3686 }
3687 }
3688
3689 async fn add_coincident(
3690 &mut self,
3691 sketch: ObjectId,
3692 coincident: Coincident,
3693 new_ast: &mut ast::Node<ast::Program>,
3694 ) -> Result<AstNodeRef, KclError> {
3695 let sketch_id = sketch;
3696 for segment in &coincident.segments {
3697 let ConstraintSegment::Segment(segment_id) = segment else {
3698 continue;
3699 };
3700 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3701 continue;
3702 };
3703 if matches!(
3704 segment_object.kind,
3705 ObjectKind::Segment {
3706 segment: Segment::ControlPointSpline(_)
3707 }
3708 ) {
3709 return Err(KclError::refactor(
3710 "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3711 .to_owned(),
3712 ));
3713 }
3714 }
3715 let segment_asts = coincident
3716 .segments
3717 .iter()
3718 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3719 .collect::<Result<Vec<_>, _>>()?;
3720 if segment_asts.len() < 2 {
3721 return Err(KclError::refactor(format!(
3722 "Coincident constraint must have at least 2 inputs, got {}",
3723 segment_asts.len()
3724 )));
3725 }
3726
3727 let coincident_ast = create_coincident_ast(segment_asts);
3729
3730 let (sketch_block_ref, _) = self.mutate_ast(
3732 new_ast,
3733 sketch_id,
3734 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3735 )?;
3736 Ok(sketch_block_ref)
3737 }
3738
3739 async fn add_distance(
3740 &mut self,
3741 sketch: ObjectId,
3742 distance: Distance,
3743 new_ast: &mut ast::Node<ast::Program>,
3744 ) -> Result<AstNodeRef, KclError> {
3745 self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3746 }
3747
3748 fn distance_constraint_ast_parts(
3749 &self,
3750 function_name: &str,
3751 distance: &Distance,
3752 new_ast: &mut ast::Node<ast::Program>,
3753 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3754 let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3755 [pt0, pt1] => [
3756 self.coincident_segment_to_ast(pt0, new_ast)?,
3757 self.coincident_segment_to_ast(pt1, new_ast)?,
3758 ],
3759 _ => {
3760 return Err(KclError::refactor(format!(
3761 "Distance constraint must have exactly 2 segments, got {}",
3762 distance.segments.len()
3763 )));
3764 }
3765 };
3766
3767 let arguments = match &distance.label_position {
3768 Some(label_position) => vec![ast::LabeledArg {
3769 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3770 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3771 }],
3772 None => Default::default(),
3773 };
3774
3775 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3776 callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3777 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3778 ast::ArrayExpression {
3779 elements: vec![segment0_ast, segment1_ast],
3780 digest: None,
3781 non_code_meta: Default::default(),
3782 },
3783 )))),
3784 arguments,
3785 digest: None,
3786 non_code_meta: Default::default(),
3787 })));
3788 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3789 value: ast::LiteralValue::Number {
3790 value: distance.distance.value,
3791 suffix: distance.distance.units,
3792 },
3793 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3794 KclError::refactor(format!(
3795 "Could not format numeric suffix: {:?}",
3796 distance.distance.units
3797 ))
3798 })?,
3799 digest: None,
3800 })));
3801
3802 Ok((call, value))
3803 }
3804
3805 fn add_distance_constraint(
3806 &mut self,
3807 sketch: ObjectId,
3808 function_name: &str,
3809 distance: Distance,
3810 new_ast: &mut ast::Node<ast::Program>,
3811 ) -> Result<AstNodeRef, KclError> {
3812 let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3813 let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3814 left: call,
3815 operator: ast::BinaryOperator::Eq,
3816 right: value,
3817 digest: None,
3818 })));
3819
3820 let (sketch_block_ref, _) = self.mutate_ast(
3821 new_ast,
3822 sketch,
3823 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3824 )?;
3825 Ok(sketch_block_ref)
3826 }
3827
3828 async fn add_angle(
3829 &mut self,
3830 sketch: ObjectId,
3831 angle: Angle,
3832 new_ast: &mut ast::Node<ast::Program>,
3833 ) -> Result<AstNodeRef, KclError> {
3834 let sketch_id = sketch;
3835 let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3836 let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3837 left: angle_call_ast,
3838 operator: ast::BinaryOperator::Eq,
3839 right: angle_value_ast,
3840 digest: None,
3841 })));
3842
3843 let (sketch_block_ref, _) = self.mutate_ast(
3845 new_ast,
3846 sketch_id,
3847 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3848 )?;
3849 Ok(sketch_block_ref)
3850 }
3851
3852 fn angle_constraint_ast_parts(
3853 &self,
3854 angle: &Angle,
3855 new_ast: &mut ast::Node<ast::Program>,
3856 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3857 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3858 return Err(KclError::refactor(format!(
3859 "Angle constraint must have exactly 2 lines, got {}",
3860 angle.lines.len()
3861 )));
3862 };
3863
3864 let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3865 let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3866 let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3867 elements: vec![l0_ast, l1_ast],
3868 digest: None,
3869 non_code_meta: Default::default(),
3870 })));
3871
3872 if angle.inverse == Some(true) && angle.sector.is_none() {
3873 return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3874 }
3875
3876 let uses_angle_dimension = angle.sector.is_some();
3877 let mut arguments = if uses_angle_dimension {
3878 vec![ast::LabeledArg {
3879 label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3880 arg: lines_ast.clone(),
3881 }]
3882 } else {
3883 Default::default()
3884 };
3885
3886 if let Some(sector) = angle.sector {
3887 arguments.push(ast::LabeledArg {
3888 label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3889 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3890 value: ast::LiteralValue::Number {
3891 value: f64::from(sector),
3892 suffix: NumericSuffix::None,
3893 },
3894 raw: sector.to_string(),
3895 digest: None,
3896 }))),
3897 });
3898 }
3899
3900 if angle.inverse == Some(true) {
3901 arguments.push(ast::LabeledArg {
3902 label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3903 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3904 value: ast::LiteralValue::Bool(true),
3905 raw: true.to_string(),
3906 digest: None,
3907 }))),
3908 });
3909 }
3910
3911 if let Some(label_position) = &angle.label_position {
3912 arguments.push(ast::LabeledArg {
3913 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3914 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3915 });
3916 }
3917
3918 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3919 callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3920 ANGLE_DIMENSION_FN
3921 } else {
3922 ANGLE_FN
3923 })),
3924 unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3925 arguments,
3926 digest: None,
3927 non_code_meta: Default::default(),
3928 })));
3929 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3930 value: ast::LiteralValue::Number {
3931 value: angle.angle.value,
3932 suffix: angle.angle.units,
3933 },
3934 raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3935 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3936 digest: None,
3937 })));
3938
3939 Ok((call, value))
3940 }
3941
3942 async fn add_tangent(
3943 &mut self,
3944 sketch: ObjectId,
3945 tangent: Tangent,
3946 new_ast: &mut ast::Node<ast::Program>,
3947 ) -> Result<AstNodeRef, KclError> {
3948 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3949 return Err(KclError::refactor(format!(
3950 "Tangent constraint must have exactly 2 segments, got {}",
3951 tangent.input.len()
3952 )));
3953 };
3954 let sketch_id = sketch;
3955
3956 let seg0_object = self
3957 .scene_graph
3958 .objects
3959 .get(seg0_id.0)
3960 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3961 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3962 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3963 };
3964 let seg0_ast = match seg0_segment {
3965 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3966 self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3967 }
3968 _ => {
3969 return Err(KclError::refactor(format!(
3970 "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3971 )));
3972 }
3973 };
3974
3975 let seg1_object = self
3976 .scene_graph
3977 .objects
3978 .get(seg1_id.0)
3979 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3980 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3981 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3982 };
3983 let seg1_ast = match seg1_segment {
3984 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3985 self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3986 }
3987 _ => {
3988 return Err(KclError::refactor(format!(
3989 "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3990 )));
3991 }
3992 };
3993
3994 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3995 let (sketch_block_ref, _) = self.mutate_ast(
3996 new_ast,
3997 sketch_id,
3998 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3999 )?;
4000 Ok(sketch_block_ref)
4001 }
4002
4003 async fn add_symmetric(
4004 &mut self,
4005 sketch: ObjectId,
4006 symmetric: Symmetric,
4007 new_ast: &mut ast::Node<ast::Program>,
4008 ) -> Result<AstNodeRef, KclError> {
4009 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4010 return Err(KclError::refactor(format!(
4011 "Symmetric constraint must have exactly 2 inputs, got {}",
4012 symmetric.input.len()
4013 )));
4014 };
4015 let sketch_id = sketch;
4016
4017 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4018 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4019 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4020
4021 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4022 let (sketch_block_ref, _) = self.mutate_ast(
4023 new_ast,
4024 sketch_id,
4025 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4026 )?;
4027 Ok(sketch_block_ref)
4028 }
4029
4030 async fn add_midpoint(
4031 &mut self,
4032 sketch: ObjectId,
4033 midpoint: Midpoint,
4034 new_ast: &mut ast::Node<ast::Program>,
4035 ) -> Result<AstNodeRef, KclError> {
4036 let sketch_id = sketch;
4037 let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4038
4039 let segment_object = self
4040 .scene_graph
4041 .objects
4042 .get(midpoint.segment.0)
4043 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4044 let ObjectKind::Segment {
4045 segment: midpoint_segment,
4046 } = &segment_object.kind
4047 else {
4048 return Err(KclError::refactor(format!(
4049 "Object must be a segment, but it was {}",
4050 segment_object.kind.human_friendly_kind_with_article()
4051 )));
4052 };
4053 let segment_ast = match midpoint_segment {
4054 Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4055 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4056 _ => {
4057 return Err(KclError::refactor(format!(
4058 "Midpoint target must be a line or arc segment but it was {}",
4059 midpoint_segment.human_friendly_kind_with_article()
4060 )));
4061 }
4062 };
4063
4064 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4065 let (sketch_block_ref, _) = self.mutate_ast(
4066 new_ast,
4067 sketch_id,
4068 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4069 )?;
4070 Ok(sketch_block_ref)
4071 }
4072
4073 async fn add_equal_radius(
4074 &mut self,
4075 sketch: ObjectId,
4076 equal_radius: EqualRadius,
4077 new_ast: &mut ast::Node<ast::Program>,
4078 ) -> Result<AstNodeRef, KclError> {
4079 if equal_radius.input.len() < 2 {
4080 return Err(KclError::refactor(format!(
4081 "equalRadius constraint must have at least 2 segments, got {}",
4082 equal_radius.input.len()
4083 )));
4084 }
4085
4086 let sketch_id = sketch;
4087 let input_asts = equal_radius
4088 .input
4089 .iter()
4090 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4091 .collect::<Result<Vec<_>, _>>()?;
4092
4093 let equal_radius_ast = create_equal_radius_ast(input_asts);
4094 let (sketch_block_ref, _) = self.mutate_ast(
4095 new_ast,
4096 sketch_id,
4097 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4098 )?;
4099 Ok(sketch_block_ref)
4100 }
4101
4102 async fn add_radius(
4103 &mut self,
4104 sketch: ObjectId,
4105 radius: Radius,
4106 new_ast: &mut ast::Node<ast::Program>,
4107 ) -> Result<AstNodeRef, KclError> {
4108 let params = ArcSizeConstraintParams {
4109 points: vec![radius.arc],
4110 function_name: RADIUS_FN,
4111 value: radius.radius.value,
4112 units: radius.radius.units,
4113 label_position: radius.label_position,
4114 constraint_type_name: "Radius",
4115 };
4116 self.add_arc_size_constraint(sketch, params, new_ast).await
4117 }
4118
4119 async fn add_diameter(
4120 &mut self,
4121 sketch: ObjectId,
4122 diameter: Diameter,
4123 new_ast: &mut ast::Node<ast::Program>,
4124 ) -> Result<AstNodeRef, KclError> {
4125 let params = ArcSizeConstraintParams {
4126 points: vec![diameter.arc],
4127 function_name: DIAMETER_FN,
4128 value: diameter.diameter.value,
4129 units: diameter.diameter.units,
4130 label_position: diameter.label_position,
4131 constraint_type_name: "Diameter",
4132 };
4133 self.add_arc_size_constraint(sketch, params, new_ast).await
4134 }
4135
4136 async fn add_fixed_constraints(
4137 &mut self,
4138 sketch: ObjectId,
4139 points: Vec<FixedPoint>,
4140 new_ast: &mut ast::Node<ast::Program>,
4141 ) -> Result<AstNodeRef, KclError> {
4142 let mut sketch_block_ref = None;
4143
4144 for fixed_point in points {
4145 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4146 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4147 .map_err(|err| KclError::refactor(err.to_string()))?;
4148
4149 let (sketch_ref, _) = self.mutate_ast(
4150 new_ast,
4151 sketch,
4152 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4153 )?;
4154 sketch_block_ref = Some(sketch_ref);
4155 }
4156
4157 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4158 }
4159
4160 async fn add_arc_size_constraint(
4161 &mut self,
4162 sketch: ObjectId,
4163 params: ArcSizeConstraintParams,
4164 new_ast: &mut ast::Node<ast::Program>,
4165 ) -> Result<AstNodeRef, KclError> {
4166 let sketch_id = sketch;
4167
4168 if params.points.len() != 1 {
4170 return Err(KclError::refactor(format!(
4171 "{} constraint must have exactly 1 argument (an arc segment), got {}",
4172 params.constraint_type_name,
4173 params.points.len()
4174 )));
4175 }
4176
4177 let arc_id = params.points[0];
4178 let arc_object = self
4179 .scene_graph
4180 .objects
4181 .get(arc_id.0)
4182 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4183 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4184 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4185 };
4186 let ref_type = match arc_segment {
4187 Segment::Arc(_) => ARC_VARIABLE,
4188 Segment::Circle(_) => CIRCLE_VARIABLE,
4189 _ => {
4190 return Err(KclError::refactor(format!(
4191 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4192 params.constraint_type_name
4193 )));
4194 }
4195 };
4196 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4198 let arguments = match ¶ms.label_position {
4199 Some(label_position) => vec![ast::LabeledArg {
4200 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4201 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4202 }],
4203 None => Default::default(),
4204 };
4205
4206 let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4208 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4209 unlabeled: Some(arc_ast),
4210 arguments,
4211 digest: None,
4212 non_code_meta: Default::default(),
4213 })));
4214 let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4215 left: call_ast,
4216 operator: ast::BinaryOperator::Eq,
4217 right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4218 value: ast::LiteralValue::Number {
4219 value: params.value,
4220 suffix: params.units,
4221 },
4222 raw: format_number_literal(params.value, params.units, None)
4223 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4224 digest: None,
4225 }))),
4226 digest: None,
4227 })));
4228
4229 let (sketch_block_ref, _) = self.mutate_ast(
4231 new_ast,
4232 sketch_id,
4233 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4234 )?;
4235 Ok(sketch_block_ref)
4236 }
4237
4238 async fn add_horizontal_distance(
4239 &mut self,
4240 sketch: ObjectId,
4241 distance: Distance,
4242 new_ast: &mut ast::Node<ast::Program>,
4243 ) -> Result<AstNodeRef, KclError> {
4244 self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4245 }
4246
4247 async fn add_vertical_distance(
4248 &mut self,
4249 sketch: ObjectId,
4250 distance: Distance,
4251 new_ast: &mut ast::Node<ast::Program>,
4252 ) -> Result<AstNodeRef, KclError> {
4253 self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4254 }
4255
4256 async fn add_horizontal(
4257 &mut self,
4258 sketch: ObjectId,
4259 horizontal: Horizontal,
4260 new_ast: &mut ast::Node<ast::Program>,
4261 ) -> Result<AstNodeRef, KclError> {
4262 let sketch_id = sketch;
4263
4264 let first_arg_ast = match horizontal {
4266 Horizontal::Line { line } => {
4267 let line_object = self
4268 .scene_graph
4269 .objects
4270 .get(line.0)
4271 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4272 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4273 let kind = line_object.kind.human_friendly_kind_with_article();
4274 return Err(KclError::refactor(format!(
4275 "This constraint only works on Segments, but you selected {kind}"
4276 )));
4277 };
4278 let Segment::Line(_) = line_segment else {
4279 return Err(KclError::refactor(format!(
4280 "Only lines can be made horizontal, but you selected {}",
4281 line_segment.human_friendly_kind_with_article(),
4282 )));
4283 };
4284 self.line_id_to_ast_reference(line, new_ast)?
4285 }
4286 Horizontal::Points { points } => {
4287 let point_asts = points
4288 .iter()
4289 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4290 .collect::<Result<Vec<_>, _>>()?;
4291 ast::ArrayExpression::new(point_asts).into()
4292 }
4293 };
4294 let horizontal_ast = create_horizontal_ast(first_arg_ast);
4296
4297 let (sketch_block_ref, _) = self.mutate_ast(
4299 new_ast,
4300 sketch_id,
4301 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4302 )?;
4303 Ok(sketch_block_ref)
4304 }
4305
4306 async fn add_lines_equal_length(
4307 &mut self,
4308 sketch: ObjectId,
4309 lines_equal_length: LinesEqualLength,
4310 new_ast: &mut ast::Node<ast::Program>,
4311 ) -> Result<AstNodeRef, KclError> {
4312 if lines_equal_length.lines.len() < 2 {
4313 return Err(KclError::refactor(format!(
4314 "Lines equal length constraint must have at least 2 lines, got {}",
4315 lines_equal_length.lines.len()
4316 )));
4317 };
4318
4319 let sketch_id = sketch;
4320
4321 let line_asts = lines_equal_length
4323 .lines
4324 .iter()
4325 .map(|line_id| {
4326 let line_object = self
4327 .scene_graph
4328 .objects
4329 .get(line_id.0)
4330 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4331 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4332 let kind = line_object.kind.human_friendly_kind_with_article();
4333 return Err(KclError::refactor(format!(
4334 "This constraint only works on Segments, but you selected {kind}"
4335 )));
4336 };
4337 let Segment::Line(_) = line_segment else {
4338 let kind = line_segment.human_friendly_kind_with_article();
4339 return Err(KclError::refactor(format!(
4340 "Only lines can be made equal length, but you selected {kind}"
4341 )));
4342 };
4343
4344 self.line_id_to_ast_reference(*line_id, new_ast)
4345 })
4346 .collect::<Result<Vec<_>, _>>()?;
4347
4348 let equal_length_ast = create_equal_length_ast(line_asts);
4350
4351 let (sketch_block_ref, _) = self.mutate_ast(
4353 new_ast,
4354 sketch_id,
4355 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4356 )?;
4357 Ok(sketch_block_ref)
4358 }
4359
4360 fn equal_radius_segment_id_to_ast_reference(
4361 &mut self,
4362 segment_id: ObjectId,
4363 new_ast: &mut ast::Node<ast::Program>,
4364 ) -> Result<ast::Expr, KclError> {
4365 let segment_object = self
4366 .scene_graph
4367 .objects
4368 .get(segment_id.0)
4369 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4370 let ObjectKind::Segment { segment } = &segment_object.kind else {
4371 return Err(KclError::refactor(format!(
4372 "Object is not a segment, it was {}",
4373 segment_object.kind.human_friendly_kind_with_article()
4374 )));
4375 };
4376
4377 let ref_type = match segment {
4378 Segment::Arc(_) => ARC_VARIABLE,
4379 Segment::Circle(_) => CIRCLE_VARIABLE,
4380 _ => {
4381 return Err(KclError::refactor(format!(
4382 "equalRadius supports only arc/circle segments, got {}",
4383 segment.human_friendly_kind_with_article()
4384 )));
4385 }
4386 };
4387
4388 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4389 }
4390
4391 fn symmetric_input_id_to_ast_reference(
4392 &mut self,
4393 segment_id: ObjectId,
4394 new_ast: &mut ast::Node<ast::Program>,
4395 ) -> Result<ast::Expr, KclError> {
4396 let segment_object = self
4397 .scene_graph
4398 .objects
4399 .get(segment_id.0)
4400 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4401 let ObjectKind::Segment { segment } = &segment_object.kind else {
4402 return Err(KclError::refactor(format!(
4403 "Object is not a segment, it was {}",
4404 segment_object.kind.human_friendly_kind_with_article()
4405 )));
4406 };
4407
4408 match segment {
4409 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4410 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4411 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4412 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4413 Segment::ControlPointSpline(_) => Err(KclError::refactor(
4414 "Symmetric does not yet support control point splines".to_owned(),
4415 )),
4416 }
4417 }
4418
4419 fn symmetric_axis_id_to_ast_reference(
4420 &mut self,
4421 segment_id: ObjectId,
4422 new_ast: &mut ast::Node<ast::Program>,
4423 ) -> Result<ast::Expr, KclError> {
4424 let segment_object = self
4425 .scene_graph
4426 .objects
4427 .get(segment_id.0)
4428 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4429 let ObjectKind::Segment { segment } = &segment_object.kind else {
4430 return Err(KclError::refactor(format!(
4431 "Object is not a segment, it was {}",
4432 segment_object.kind.human_friendly_kind_with_article()
4433 )));
4434 };
4435 match segment {
4436 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4437 _ => Err(KclError::refactor(format!(
4438 "Symmetric axis must be a line, got {}",
4439 segment.human_friendly_kind_with_article()
4440 ))),
4441 }
4442 }
4443
4444 async fn add_parallel(
4445 &mut self,
4446 sketch: ObjectId,
4447 parallel: Parallel,
4448 new_ast: &mut ast::Node<ast::Program>,
4449 ) -> Result<AstNodeRef, KclError> {
4450 if parallel.lines.len() < 2 {
4451 return Err(KclError::refactor(format!(
4452 "Parallel constraint must have at least 2 lines, got {}",
4453 parallel.lines.len()
4454 )));
4455 };
4456
4457 let sketch_id = sketch;
4458
4459 let line_asts = parallel
4460 .lines
4461 .iter()
4462 .map(|line_id| {
4463 let line_object = self
4464 .scene_graph
4465 .objects
4466 .get(line_id.0)
4467 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4468 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4469 let kind = line_object.kind.human_friendly_kind_with_article();
4470 return Err(KclError::refactor(format!(
4471 "This constraint only works on Segments, but you selected {kind}"
4472 )));
4473 };
4474 let Segment::Line(_) = line_segment else {
4475 let kind = line_segment.human_friendly_kind_with_article();
4476 return Err(KclError::refactor(format!(
4477 "Only lines can be made parallel, but you selected {kind}"
4478 )));
4479 };
4480
4481 self.line_id_to_ast_reference(*line_id, new_ast)
4482 })
4483 .collect::<Result<Vec<_>, _>>()?;
4484
4485 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4486 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4487 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4488 ast::ArrayExpression {
4489 elements: line_asts,
4490 digest: None,
4491 non_code_meta: Default::default(),
4492 },
4493 )))),
4494 arguments: Default::default(),
4495 digest: None,
4496 non_code_meta: Default::default(),
4497 })));
4498
4499 let (sketch_block_ref, _) = self.mutate_ast(
4500 new_ast,
4501 sketch_id,
4502 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4503 )?;
4504 Ok(sketch_block_ref)
4505 }
4506
4507 async fn add_perpendicular(
4508 &mut self,
4509 sketch: ObjectId,
4510 perpendicular: Perpendicular,
4511 new_ast: &mut ast::Node<ast::Program>,
4512 ) -> Result<AstNodeRef, KclError> {
4513 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4514 .await
4515 }
4516
4517 async fn add_lines_at_angle_constraint(
4518 &mut self,
4519 sketch: ObjectId,
4520 angle_kind: LinesAtAngleKind,
4521 lines: Vec<ObjectId>,
4522 new_ast: &mut ast::Node<ast::Program>,
4523 ) -> Result<AstNodeRef, KclError> {
4524 let &[line0_id, line1_id] = lines.as_slice() else {
4525 return Err(KclError::refactor(format!(
4526 "{} constraint must have exactly 2 lines, got {}",
4527 angle_kind.to_function_name(),
4528 lines.len()
4529 )));
4530 };
4531
4532 let sketch_id = sketch;
4533
4534 let line0_object = self
4536 .scene_graph
4537 .objects
4538 .get(line0_id.0)
4539 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4540 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4541 let kind = line0_object.kind.human_friendly_kind_with_article();
4542 return Err(KclError::refactor(format!(
4543 "This constraint only works on Segments, but you selected {kind}"
4544 )));
4545 };
4546 let Segment::Line(_) = line0_segment else {
4547 return Err(KclError::refactor(format!(
4548 "Only lines can be made {}, but you selected {}",
4549 angle_kind.to_function_name(),
4550 line0_segment.human_friendly_kind_with_article(),
4551 )));
4552 };
4553 let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4554
4555 let line1_object = self
4556 .scene_graph
4557 .objects
4558 .get(line1_id.0)
4559 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4560 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4561 let kind = line1_object.kind.human_friendly_kind_with_article();
4562 return Err(KclError::refactor(format!(
4563 "This constraint only works on Segments, but you selected {kind}"
4564 )));
4565 };
4566 let Segment::Line(_) = line1_segment else {
4567 return Err(KclError::refactor(format!(
4568 "Only lines can be made {}, but you selected {}",
4569 angle_kind.to_function_name(),
4570 line1_segment.human_friendly_kind_with_article(),
4571 )));
4572 };
4573 let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4574
4575 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4577 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4578 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4579 ast::ArrayExpression {
4580 elements: vec![line0_ast, line1_ast],
4581 digest: None,
4582 non_code_meta: Default::default(),
4583 },
4584 )))),
4585 arguments: Default::default(),
4586 digest: None,
4587 non_code_meta: Default::default(),
4588 })));
4589
4590 let (sketch_block_ref, _) = self.mutate_ast(
4592 new_ast,
4593 sketch_id,
4594 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4595 )?;
4596 Ok(sketch_block_ref)
4597 }
4598
4599 async fn add_vertical(
4600 &mut self,
4601 sketch: ObjectId,
4602 vertical: Vertical,
4603 new_ast: &mut ast::Node<ast::Program>,
4604 ) -> Result<AstNodeRef, KclError> {
4605 let sketch_id = sketch;
4606
4607 let first_arg_ast = match vertical {
4608 Vertical::Line { line } => {
4609 let line_object = self
4611 .scene_graph
4612 .objects
4613 .get(line.0)
4614 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4615 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4616 let kind = line_object.kind.human_friendly_kind_with_article();
4617 return Err(KclError::refactor(format!(
4618 "This constraint only works on Segments, but you selected {kind}"
4619 )));
4620 };
4621 let Segment::Line(_) = line_segment else {
4622 return Err(KclError::refactor(format!(
4623 "Only lines can be made vertical, but you selected {}",
4624 line_segment.human_friendly_kind_with_article()
4625 )));
4626 };
4627 self.line_id_to_ast_reference(line, new_ast)?
4628 }
4629 Vertical::Points { points } => {
4630 let point_asts = points
4631 .iter()
4632 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4633 .collect::<Result<Vec<_>, _>>()?;
4634 ast::ArrayExpression::new(point_asts).into()
4635 }
4636 };
4637 let vertical_ast = create_vertical_ast(first_arg_ast);
4639
4640 let (sketch_block_ref, _) = self.mutate_ast(
4642 new_ast,
4643 sketch_id,
4644 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4645 )?;
4646 Ok(sketch_block_ref)
4647 }
4648
4649 async fn execute_after_add_constraint(
4650 &mut self,
4651 ctx: &ExecutorContext,
4652 sketch_id: ObjectId,
4653 sketch_block_ref: AstNodeRef,
4654 new_ast: &mut ast::Node<ast::Program>,
4655 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4656 let new_source = source_from_ast(new_ast);
4658 let new_program = parse_frontend_mutation_source(
4660 &new_source,
4661 "Error parsing KCL source after adding constraint",
4662 "No AST produced after adding constraint",
4663 )?;
4664 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4665 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4666 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4667 )))
4668 })?;
4669
4670 let mut truncated_program = new_program.clone();
4673 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4674 .map_err(KclErrorWithOutputs::no_outputs)?;
4675
4676 let outcome = ctx
4678 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4679 .await?;
4680
4681 let new_object_ids = {
4682 let constraint_id = outcome
4684 .source_range_to_object
4685 .get(&constraint_node_ref.range)
4686 .copied()
4687 .ok_or_else(|| {
4688 KclErrorWithOutputs::from_error_outcome(
4689 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4690 outcome.clone(),
4691 )
4692 })?;
4693 vec![constraint_id]
4694 };
4695
4696 self.program = new_program;
4699
4700 let outcome = self.update_state_after_exec(outcome, true);
4702
4703 let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4704 let scene_graph_delta = SceneGraphDelta {
4705 new_graph: self.scene_graph_for_ui(),
4706 invalidates_ids: false,
4707 new_objects: new_object_ids,
4708 exec_outcome: outcome,
4709 };
4710 Ok((src_delta, scene_graph_delta))
4711 }
4712
4713 fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4714 let commit_failure = || {
4715 KclErrorWithOutputs::from_error_outcome(
4716 KclError::refactor(format!("Could not update KCL after {operation}.")),
4717 outcome.clone(),
4718 )
4719 };
4720
4721 let default_length_unit = self.default_length_unit();
4722 let mut settled_ast = self.program.ast.clone();
4723 let mut committed_solver_value = false;
4724 for (var_range, node_path, value) in &outcome.var_solutions {
4725 let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4726 return Err(commit_failure());
4727 };
4728 let new_value = match &lookup {
4729 Some(current_literal) => {
4730 if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4731 continue;
4732 }
4733 preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4734 }
4735 None => {
4736 Number {
4740 value: number_value_in_default_length_units(*value, default_length_unit),
4741 units: default_length_unit.into(),
4742 }
4743 }
4744 };
4745 committed_solver_value = true;
4746 let source_ref = SourceRef::Simple {
4747 range: *var_range,
4748 node_path: node_path.clone(),
4749 };
4750 mutate_ast_node_by_source_ref(
4751 &mut settled_ast,
4752 &source_ref,
4753 AstMutateCommand::EditVarInitialValue { value: new_value },
4754 )
4755 .map_err(|_| commit_failure())?;
4756 }
4757
4758 if !committed_solver_value {
4759 return Ok(SourceDelta {
4760 text: self.program.original_file_contents.clone(),
4761 });
4762 }
4763
4764 let settled_source = source_from_ast(&settled_ast);
4765 let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4766 if !errors.is_empty() {
4767 return Err(commit_failure());
4768 }
4769 let Some(settled_program) = settled_program else {
4770 return Err(commit_failure());
4771 };
4772
4773 self.program = settled_program;
4774
4775 Ok(SourceDelta { text: settled_source })
4776 }
4777
4778 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4780 if segment_ids_set.contains(&segment_id) {
4781 return true;
4782 }
4783
4784 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4785 return false;
4786 };
4787 let ObjectKind::Segment { segment } = &segment_object.kind else {
4788 return false;
4789 };
4790 let Segment::Point(point) = segment else {
4791 return false;
4792 };
4793
4794 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4795 }
4796
4797 fn remaining_constraint_segments(
4798 &self,
4799 segments: &[ConstraintSegment],
4800 segment_ids_set: &AhashIndexSet<ObjectId>,
4801 ) -> Vec<ConstraintSegment> {
4802 segments
4803 .iter()
4804 .copied()
4805 .filter(|segment| match segment {
4806 ConstraintSegment::Origin(_) => true,
4807 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4808 })
4809 .collect()
4810 }
4811
4812 fn find_referenced_constraints(
4813 &self,
4814 sketch_id: ObjectId,
4815 segment_ids_set: &AhashIndexSet<ObjectId>,
4816 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4817 let sketch_object = self
4819 .scene_graph
4820 .objects
4821 .get(sketch_id.0)
4822 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4823 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4824 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4825 };
4826 let segment_or_owner_matches = |segment_id: ObjectId| {
4827 if segment_ids_set.contains(&segment_id) {
4828 return true;
4829 }
4830 let segment_object = self.scene_graph.objects.get(segment_id.0);
4831 if let Some(obj) = segment_object
4832 && let ObjectKind::Segment { segment } = &obj.kind
4833 {
4834 match segment {
4835 Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4836 Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4837 _ => false,
4838 }
4839 } else {
4840 false
4841 }
4842 };
4843 let mut constraint_ids_set = AhashIndexSet::default();
4844 for constraint_id in &sketch.constraints {
4845 let constraint_object = self
4846 .scene_graph
4847 .objects
4848 .get(constraint_id.0)
4849 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4850 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4851 return Err(KclError::refactor(format!(
4852 "Object is not a constraint, it is {}",
4853 constraint_object.kind.human_friendly_kind_with_article()
4854 )));
4855 };
4856 let depends_on_segment = match constraint {
4857 Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4858 Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4859 Constraint::Fixed(fixed) => fixed
4860 .points
4861 .iter()
4862 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4863 Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4864 Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4865 Constraint::EqualRadius(equal_radius) => {
4866 equal_radius.input.iter().copied().any(segment_or_owner_matches)
4867 }
4868 Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4869 Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4870 Constraint::Horizontal(h) => match h {
4871 Horizontal::Line { line } => segment_or_owner_matches(*line),
4872 Horizontal::Points { points } => points.iter().any(|point| match point {
4873 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4874 ConstraintSegment::Origin(_) => false,
4875 }),
4876 },
4877 Constraint::Vertical(v) => match v {
4878 Vertical::Line { line } => segment_or_owner_matches(*line),
4879 Vertical::Points { points } => points.iter().any(|point| match point {
4880 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4881 ConstraintSegment::Origin(_) => false,
4882 }),
4883 },
4884 Constraint::LinesEqualLength(lines_equal_length) => {
4885 lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4886 }
4887 Constraint::Midpoint(midpoint) => {
4888 segment_or_owner_matches(midpoint.segment)
4889 || matches!(
4890 midpoint.point,
4891 ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4892 )
4893 }
4894 Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4895 Constraint::Perpendicular(perpendicular) => {
4896 perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4897 }
4898 Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4899 Constraint::Symmetric(symmetric) => {
4900 segment_or_owner_matches(symmetric.axis)
4901 || symmetric.input.iter().copied().any(segment_or_owner_matches)
4902 }
4903 Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4904 };
4905 if depends_on_segment {
4906 constraint_ids_set.insert(*constraint_id);
4907 }
4908 }
4909 Ok(constraint_ids_set)
4910 }
4911
4912 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4913 let mut outcome = outcome;
4914 self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4915 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4916
4917 if freedom_analysis_ran {
4918 self.point_freedom_cache.clear();
4921 for new_obj in &new_objects {
4922 if let ObjectKind::Segment {
4923 segment: crate::front::Segment::Point(point),
4924 } = &new_obj.kind
4925 {
4926 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4927 }
4928 }
4929 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4930 self.scene_graph.objects = new_objects;
4932 } else {
4933 for old_obj in &self.scene_graph.objects {
4936 if let ObjectKind::Segment {
4937 segment: crate::front::Segment::Point(point),
4938 } = &old_obj.kind
4939 {
4940 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4941 }
4942 }
4943
4944 let mut updated_objects = Vec::with_capacity(new_objects.len());
4946 for new_obj in new_objects {
4947 let mut obj = new_obj;
4948 if let ObjectKind::Segment {
4949 segment: crate::front::Segment::Point(point),
4950 } = &mut obj.kind
4951 {
4952 let new_freedom = point.freedom;
4953 match new_freedom {
4959 Freedom::Free => {
4960 match self.point_freedom_cache.get(&obj.id).copied() {
4961 Some(Freedom::Conflict) => {
4962 }
4965 Some(Freedom::Fixed) => {
4966 point.freedom = Freedom::Fixed;
4968 }
4969 Some(Freedom::Free) => {
4970 }
4972 None => {
4973 }
4975 }
4976 }
4977 Freedom::Fixed => {
4978 }
4980 Freedom::Conflict => {
4981 }
4983 }
4984 self.point_freedom_cache.insert(obj.id, point.freedom);
4986 }
4987 updated_objects.push(obj);
4988 }
4989
4990 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4991 self.scene_graph.objects = updated_objects;
4992 }
4993 outcome
4994 }
4995
4996 fn mutate_ast(
4997 &mut self,
4998 ast: &mut ast::Node<ast::Program>,
4999 object_id: ObjectId,
5000 command: AstMutateCommand,
5001 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5002 let sketch_object = self
5003 .scene_graph
5004 .objects
5005 .get(object_id.0)
5006 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5007 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5008 }
5009
5010 fn mutate_constraint_label_position(
5011 &mut self,
5012 ast: &mut ast::Node<ast::Program>,
5013 constraint_id: ObjectId,
5014 label_position: Point2d<Number>,
5015 ) -> Result<(), KclError> {
5016 let object = self
5017 .scene_graph
5018 .objects
5019 .get(constraint_id.0)
5020 .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5021 if !matches!(
5022 &object.kind,
5023 ObjectKind::Constraint {
5024 constraint: Constraint::Distance(_)
5025 | Constraint::HorizontalDistance(_)
5026 | Constraint::VerticalDistance(_)
5027 | Constraint::Radius(_)
5028 | Constraint::Diameter(_)
5029 | Constraint::Angle(_),
5030 }
5031 ) {
5032 return Err(KclError::refactor(format!(
5033 "Object does not support labelPosition: {constraint_id:?}"
5034 )));
5035 }
5036
5037 let label_position = to_ast_point2d_number(&label_position)
5038 .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5039 self.mutate_ast(
5040 ast,
5041 constraint_id,
5042 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5043 )?;
5044 Ok(())
5045 }
5046}
5047
5048fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5049 let sketch_object = scene_graph
5051 .objects
5052 .get(sketch_id.0)
5053 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5054 let ObjectKind::Sketch(_) = &sketch_object.kind else {
5055 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5056 };
5057 expect_single_node_ref(sketch_object)
5058}
5059
5060fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5061 match &object.source {
5062 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5063 range: *range,
5064 node_path: node_path.clone(),
5065 }),
5066 SourceRef::BackTrace { ranges } => {
5067 let [range] = ranges.as_slice() else {
5068 return Err(KclError::refactor(format!(
5069 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5070 ranges.len()
5071 )));
5072 };
5073 Ok(AstNodeRef {
5074 range: range.0,
5075 node_path: range.1.clone(),
5076 })
5077 }
5078 }
5079}
5080
5081fn only_sketch_block_from_range(
5084 ast: &mut ast::Node<ast::Program>,
5085 sketch_block_range: SourceRange,
5086 edit_kind: ChangeKind,
5087) -> Result<(), KclError> {
5088 let r1 = sketch_block_range;
5089 let matches_range = |r2: SourceRange| -> bool {
5090 match edit_kind {
5093 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5094 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5096 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5097 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5099 }
5100 };
5101 let mut found = false;
5102 for item in ast.body.iter_mut() {
5103 match item {
5104 ast::BodyItem::ImportStatement(_) => {}
5105 ast::BodyItem::ExpressionStatement(node) => {
5106 if matches_range(SourceRange::from(&*node))
5107 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5108 {
5109 sketch_block.is_being_edited = true;
5110 found = true;
5111 break;
5112 }
5113 }
5114 ast::BodyItem::VariableDeclaration(node) => {
5115 if matches_range(SourceRange::from(&node.declaration.init))
5116 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5117 {
5118 sketch_block.is_being_edited = true;
5119 found = true;
5120 break;
5121 }
5122 }
5123 ast::BodyItem::TypeDeclaration(_) => {}
5124 ast::BodyItem::ReturnStatement(node) => {
5125 if matches_range(SourceRange::from(&node.argument))
5126 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5127 {
5128 sketch_block.is_being_edited = true;
5129 found = true;
5130 break;
5131 }
5132 }
5133 }
5134 }
5135 if !found {
5136 return Err(KclError::refactor(format!(
5137 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5138 )));
5139 }
5140
5141 Ok(())
5142}
5143
5144fn only_sketch_block(
5145 ast: &mut ast::Node<ast::Program>,
5146 sketch_block_ref: &AstNodeRef,
5147 edit_kind: ChangeKind,
5148) -> Result<(), KclError> {
5149 let Some(target_node_path) = &sketch_block_ref.node_path else {
5150 #[cfg(target_arch = "wasm32")]
5151 web_sys::console::warn_1(
5152 &format!(
5153 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5154 sketch_block_ref
5155 )
5156 .into(),
5157 );
5158 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5159 };
5160 struct MarkSketchBlockBeingEdited<'a> {
5161 target_node_path: &'a ast::NodePath,
5162 }
5163
5164 impl Visitor for MarkSketchBlockBeingEdited<'_> {
5165 type Break = ();
5166 type Continue = ();
5167
5168 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5169 if let NodeMut::SketchBlock(sketch_block) = node
5170 && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5171 {
5172 sketch_block.is_being_edited = true;
5173 return TraversalReturn::new_break(());
5174 }
5175 TraversalReturn::new_continue(())
5176 }
5177
5178 fn finish(&mut self, _node: NodeMut<'_>) {}
5179 }
5180
5181 let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5182 let found = dfs_mut(ast, &mut marker).is_break();
5183 if !found {
5184 return Err(KclError::refactor(format!(
5185 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5186 )));
5187 }
5188
5189 Ok(())
5190}
5191
5192fn sketch_on_ast_expr(
5193 ast: &mut ast::Node<ast::Program>,
5194 scene_graph: &SceneGraph,
5195 solid_references: &HashMap<Uuid, SolidAstReference>,
5196 on: &Plane,
5197) -> Result<ast::Expr, KclError> {
5198 match on {
5199 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5200 Plane::Object(object_id) => {
5201 let on_object = scene_graph
5202 .objects
5203 .get(object_id.0)
5204 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5205 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5206 return Ok(face_expr);
5207 }
5208 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5209 }
5210 Plane::PrimitiveFace(face) => {
5211 let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5212 KclError::refactor(format!(
5213 "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5214 face.solid_id
5215 ))
5216 })?;
5217 let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5218 Ok(create_face_of_ast(solid_expr, face_id_expr))
5219 }
5220 }
5221}
5222
5223fn solid_references_from_variables(
5224 ast: &ast::Node<ast::Program>,
5225 variables: &IndexMap<String, KclValueView>,
5226) -> HashMap<Uuid, SolidAstReference> {
5227 let mut references = HashMap::new();
5228
5229 for item in &ast.body {
5232 let ast::BodyItem::VariableDeclaration(declaration) = item else {
5233 continue;
5234 };
5235 let name = &declaration.declaration.id.name;
5236 let Some(value) = variables.get(name) else {
5237 continue;
5238 };
5239
5240 match value {
5241 KclValueView::Solid { value } => {
5242 references.insert(
5243 value.id,
5244 SolidAstReference {
5245 variable_name: name.clone(),
5246 output_index: None,
5247 },
5248 );
5249 }
5250 KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5251 for (output_index, entry) in value.iter().enumerate() {
5252 if let KclValueView::Solid { value } = entry {
5253 references.insert(
5254 value.id,
5255 SolidAstReference {
5256 variable_name: name.clone(),
5257 output_index: Some(output_index),
5258 },
5259 );
5260 }
5261 }
5262 }
5263 _ => {}
5264 }
5265 }
5266
5267 references
5268}
5269
5270fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5271 let reference = solid_references.get(&solid_id)?;
5272 let solid_expr = ast_name_expr(reference.variable_name.clone());
5273 Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5274}
5275
5276fn sketch_face_of_scene_object_ast_expr(
5277 ast: &mut ast::Node<ast::Program>,
5278 on_object: &crate::front::Object,
5279) -> Result<Option<ast::Expr>, KclError> {
5280 match &on_object.kind {
5281 ObjectKind::Wall(wall) => {
5282 let solid_ref = get_or_insert_ast_reference(
5283 ast,
5284 &source_ref_from_source_ref_range(&wall.source.solid),
5285 "solid",
5286 None,
5287 )?;
5288 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5289 return Err(KclError::refactor(format!(
5290 "Could not resolve solid reference for selected wall: artifact_id={:?}",
5291 on_object.artifact_id
5292 )));
5293 };
5294 let solid_expr = indexed_solid_expr_for_sweep_output(
5295 ast_name_expr(solid_name_expr.name.name.clone()),
5296 wall.solid_output_index,
5297 );
5298 let sweep_ref = get_or_insert_ast_reference(
5299 ast,
5300 &source_ref_from_source_ref_range(&wall.source.sweep),
5301 "solid",
5302 None,
5303 )?;
5304 let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5305 return Err(KclError::refactor(format!(
5306 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5307 on_object.artifact_id
5308 )));
5309 };
5310 let sweep_name = sweep_name_expr.name.name.clone();
5311 let segment_ref = get_or_insert_ast_reference(
5312 ast,
5313 &source_ref_from_source_ref_range(&wall.source.segment),
5314 LINE_VARIABLE,
5315 None,
5316 )?;
5317
5318 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5319 wall.source
5320 .path
5321 .as_ref()
5322 .and_then(|path_source| region_name_from_path_source(ast, path_source))
5323 }) {
5324 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5325 return Err(KclError::refactor(format!(
5326 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5327 on_object.artifact_id
5328 )));
5329 };
5330 create_member_expression(
5331 create_member_expression(ast_name_expr(region_name), "tags"),
5332 &segment_name_expr.name.name,
5333 )
5334 } else {
5335 segment_ref
5336 };
5337
5338 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5339 }
5340 ObjectKind::Cap(cap) => {
5341 let solid_ref =
5342 get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5343 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5344 return Err(KclError::refactor(format!(
5345 "Could not resolve solid reference for selected cap: artifact_id={:?}",
5346 on_object.artifact_id
5347 )));
5348 };
5349 let solid_expr = indexed_solid_expr_for_sweep_output(
5350 ast_name_expr(solid_name_expr.name.name.clone()),
5351 cap.solid_output_index,
5352 );
5353 let face_expr = match cap.kind {
5355 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5356 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5357 };
5358
5359 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5360 }
5361 _ => Ok(None),
5362 }
5363}
5364
5365fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5366 match solid_output_index {
5367 Some(output_index) => create_index_expression(solid_expr, output_index),
5368 None => solid_expr,
5369 }
5370}
5371
5372fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5373 SourceRef::Simple {
5374 range: source.range,
5375 node_path: source.node_path.clone(),
5376 }
5377}
5378
5379fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5380 let source_ref = source_ref_from_source_ref_range(path_source);
5381 let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5382 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5383 return None;
5384 };
5385 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5386 return None;
5387 };
5388 if region_call.callee.name.name != "region" {
5389 return None;
5390 }
5391 Some(candidate)
5392}
5393
5394fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5395 let mut current_id = source_id;
5396 let mut current_composite = None;
5397 let mut visited = HashSet::new();
5398
5399 while visited.insert(current_id) {
5400 let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5401
5402 let Some(composite_id) = next_composite_id else {
5403 break;
5404 };
5405 let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5406 break;
5407 };
5408
5409 current_id = composite.id;
5410 current_composite = Some(composite);
5411
5412 if !composite.consumed {
5413 break;
5414 }
5415 }
5416
5417 current_composite.map(|composite| &composite.code_ref)
5418}
5419
5420fn downstream_composite_id_for_solid_source(
5421 artifact_graph: &ArtifactGraph,
5422 source_id: ArtifactId,
5423) -> Option<ArtifactId> {
5424 if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5426 && let Some(composite_id) = path.composite_solid_id
5427 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5428 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5429 {
5430 return Some(composite_id);
5431 }
5432
5433 for artifact in artifact_graph.values() {
5435 if let Artifact::Path(path) = artifact
5436 && path.sweep_id == Some(source_id)
5437 && let Some(composite_id) = path.composite_solid_id
5438 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5439 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5440 {
5441 return Some(composite_id);
5442 }
5443 }
5444
5445 artifact_graph.values().find_map(|artifact| {
5447 let Artifact::CompositeSolid(composite) = artifact else {
5448 return None;
5449 };
5450 composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5451 })
5452}
5453
5454fn composite_contains_path_input(
5455 solid_ids: &[ArtifactId],
5456 tool_ids: &[ArtifactId],
5457 path_id: ArtifactId,
5458 solid2d_id: Option<ArtifactId>,
5459) -> bool {
5460 composite_contains_input(solid_ids, tool_ids, path_id)
5461 || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5462}
5463
5464fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5465 solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5466}
5467
5468fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5469 let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5470 SourceRefRange {
5471 range: code_ref.range,
5472 node_path,
5473 }
5474}
5475
5476fn solid_output_index_for_sweep(
5477 artifact_graph: &ArtifactGraph,
5478 sweep_id: ArtifactId,
5479 sweep_code_ref: &CodeRef,
5480) -> Option<usize> {
5481 if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5484 return None;
5485 }
5486
5487 let sibling_sweeps = artifact_graph
5488 .values()
5489 .filter_map(|artifact| match artifact {
5490 Artifact::Sweep(sweep)
5491 if sweep.code_ref.range == sweep_code_ref.range
5492 && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5493 {
5494 Some(sweep)
5495 }
5496 _ => None,
5497 })
5498 .collect::<Vec<_>>();
5499
5500 if sibling_sweeps.len() <= 1 {
5501 return None;
5502 }
5503
5504 sibling_sweeps
5505 .iter()
5506 .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5507}
5508
5509fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5510 let mut existing_artifact_ids = scene_objects
5511 .iter()
5512 .map(|object| object.artifact_id)
5513 .collect::<HashSet<_>>();
5514
5515 for artifact in artifact_graph.values() {
5516 match artifact {
5517 Artifact::Wall(wall) => {
5518 if existing_artifact_ids.contains(&wall.id) {
5519 continue;
5520 }
5521
5522 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5523 Artifact::Segment(segment) => Some(segment),
5524 _ => None,
5525 }) else {
5526 continue;
5527 };
5528 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5529 Artifact::Sweep(sweep) => Some(sweep),
5530 _ => None,
5531 }) else {
5532 continue;
5533 };
5534 let source_segment = segment
5535 .original_seg_id
5536 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5537 .and_then(|artifact| match artifact {
5538 Artifact::Segment(segment) => Some(segment),
5539 _ => None,
5540 })
5541 .unwrap_or(segment);
5542 let solid_code_ref =
5543 downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5544 let path_code_ref = artifact_graph
5545 .get(&segment.path_id)
5546 .or_else(|| artifact_graph.get(&sweep.path_id))
5547 .and_then(|artifact| match artifact {
5548 Artifact::Path(path) => Some(&path.code_ref),
5549 _ => None,
5550 });
5551 let source = WallSource {
5552 solid: code_ref_source_ref_range(solid_code_ref),
5553 sweep: code_ref_source_ref_range(&sweep.code_ref),
5554 path: path_code_ref.map(code_ref_source_ref_range),
5555 segment: code_ref_source_ref_range(&source_segment.code_ref),
5556 };
5557 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5558 && solid_code_ref.node_path == sweep.code_ref.node_path)
5559 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5560 .flatten();
5561 let object_source = source_ref_from_source_ref_range(&source.solid);
5562 let id = ObjectId(scene_objects.len());
5563 scene_objects.push(crate::front::Object {
5564 id,
5565 kind: ObjectKind::Wall(crate::frontend::api::Wall {
5566 id,
5567 source,
5568 solid_output_index,
5569 }),
5570 label: Default::default(),
5571 comments: Default::default(),
5572 artifact_id: wall.id,
5573 source: object_source,
5574 });
5575 existing_artifact_ids.insert(wall.id);
5576 }
5577 Artifact::Cap(cap) => {
5578 if existing_artifact_ids.contains(&cap.id) {
5579 continue;
5580 }
5581
5582 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5583 Artifact::Sweep(sweep) => Some(sweep),
5584 _ => None,
5585 }) else {
5586 continue;
5587 };
5588 let id = ObjectId(scene_objects.len());
5589 let kind = match cap.sub_type {
5590 CapSubType::Start => crate::frontend::api::CapKind::Start,
5591 CapSubType::End => crate::frontend::api::CapKind::End,
5592 };
5593 let solid_code_ref =
5594 downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5595 let source = CapSource {
5596 solid: code_ref_source_ref_range(solid_code_ref),
5597 sweep: code_ref_source_ref_range(&sweep.code_ref),
5598 };
5599 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5600 && solid_code_ref.node_path == sweep.code_ref.node_path)
5601 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5602 .flatten();
5603 let object_source = source_ref_from_source_ref_range(&source.solid);
5604 scene_objects.push(crate::front::Object {
5605 id,
5606 kind: ObjectKind::Cap(crate::frontend::api::Cap {
5607 id,
5608 kind,
5609 source,
5610 solid_output_index,
5611 }),
5612 label: Default::default(),
5613 comments: Default::default(),
5614 artifact_id: cap.id,
5615 source: object_source,
5616 });
5617 existing_artifact_ids.insert(cap.id);
5618 }
5619 _ => {}
5620 }
5621 }
5622}
5623
5624fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5625 use crate::engine::PlaneName;
5626
5627 match name {
5628 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5629 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5630 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5631 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5632 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5633 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5634 }
5635}
5636
5637fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5638 ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5639 ast::UnaryOperator::Neg,
5640 ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5641 )))
5642}
5643
5644fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5645 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5646 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5647 unlabeled: Some(solid_expr),
5648 arguments: vec![ast::LabeledArg {
5649 label: Some(ast::Identifier::new("face")),
5650 arg: face_expr,
5651 }],
5652 digest: None,
5653 non_code_meta: Default::default(),
5654 })))
5655}
5656
5657fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5658 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5659 callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5660 unlabeled: Some(solid_expr),
5661 arguments: vec![ast::LabeledArg {
5662 label: Some(ast::Identifier::new("index")),
5663 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5664 ast::NumericLiteral {
5665 value: index as f64,
5666 suffix: NumericSuffix::None,
5667 raw: index.to_string(),
5668 digest: None,
5669 },
5670 )))),
5671 }],
5672 digest: None,
5673 non_code_meta: Default::default(),
5674 })))
5675}
5676
5677fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5678 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5679 return None;
5680 };
5681 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5682 return None;
5683 };
5684 if !matches!(
5685 sweep_call.callee.name.name.as_str(),
5686 "extrude" | "revolve" | "sweep" | "loft"
5687 ) {
5688 return None;
5689 }
5690 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5691 return None;
5692 };
5693 let candidate = region_name_expr.name.name.clone();
5694 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5695 return None;
5696 };
5697 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5698 return None;
5699 };
5700 if region_call.callee.name.name != "region" {
5701 return None;
5702 }
5703 Some(candidate)
5704}
5705
5706fn get_or_insert_ast_reference(
5713 ast: &mut ast::Node<ast::Program>,
5714 source_ref: &SourceRef,
5715 prefix: &str,
5716 property: Option<&str>,
5717) -> Result<ast::Expr, KclError> {
5718 let command = AstMutateCommand::AddVariableDeclaration {
5719 prefix: prefix.to_owned(),
5720 };
5721 let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5722 Ok((_, ret)) => ret,
5723 Err(err) => {
5724 if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5725 AstMutateCommandReturn::Name(var_name)
5726 } else {
5727 return Err(err);
5728 }
5729 }
5730 };
5731 let AstMutateCommandReturn::Name(var_name) = ret else {
5732 return Err(KclError::refactor(
5733 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5734 ));
5735 };
5736 let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5737 let Some(property) = property else {
5738 return Ok(var_expr);
5740 };
5741
5742 Ok(create_member_expression(var_expr, property))
5743}
5744
5745fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5746 let source_range = match source_ref {
5747 SourceRef::Simple { range, .. } => *range,
5748 SourceRef::BackTrace { ranges } => {
5749 let [range] = ranges.as_slice() else {
5750 return None;
5751 };
5752 range.0
5753 }
5754 };
5755 ast.body.iter().find_map(|item| {
5756 let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5757 return None;
5758 };
5759 let init_range = SourceRange::from(&var_decl.declaration.init);
5760 let source_is_inside_init = init_range.module_id() == source_range.module_id()
5761 && init_range.start() <= source_range.start()
5762 && source_range.end() <= init_range.end();
5763 if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5764 && init_range != source_range
5765 && source_is_inside_init
5766 {
5767 return None;
5768 }
5769 source_is_inside_init.then(|| var_decl.name().to_owned())
5770 })
5771}
5772
5773fn mutate_ast_node_by_source_ref(
5774 ast: &mut ast::Node<ast::Program>,
5775 source_ref: &SourceRef,
5776 command: AstMutateCommand,
5777) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5778 let (source_range, node_path) = match source_ref {
5779 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5780 SourceRef::BackTrace { ranges } => {
5781 let [range] = ranges.as_slice() else {
5782 return Err(KclError::refactor(format!(
5783 "Expected single source ref, got {}; ranges={ranges:#?}",
5784 ranges.len(),
5785 )));
5786 };
5787 (range.0, range.1.clone())
5788 }
5789 };
5790 let mut context = AstMutateContext {
5791 source_range,
5792 node_path,
5793 command,
5794 defined_names_stack: Default::default(),
5795 };
5796 let control = dfs_mut(ast, &mut context);
5797 match control {
5798 ControlFlow::Continue(_) => Err(KclError::refactor(
5799 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5800 )),
5801 ControlFlow::Break(break_value) => break_value,
5802 }
5803}
5804
5805#[derive(Debug)]
5806struct AstMutateContext {
5807 source_range: SourceRange,
5808 node_path: Option<ast::NodePath>,
5809 command: AstMutateCommand,
5810 defined_names_stack: Vec<HashSet<String>>,
5811}
5812
5813#[derive(Debug)]
5814#[allow(clippy::large_enum_variant)]
5815enum AstMutateCommand {
5816 AddSketchBlockExprStmt {
5818 expr: ast::Expr,
5819 },
5820 AddSketchBlockVarDecl {
5822 prefix: String,
5823 expr: ast::Expr,
5824 },
5825 AddVariableDeclaration {
5826 prefix: String,
5827 },
5828 EditPoint {
5829 at: ast::Expr,
5830 },
5831 EditLine {
5832 start: ast::Expr,
5833 end: ast::Expr,
5834 construction: Option<bool>,
5835 },
5836 EditArc {
5837 start: ast::Expr,
5838 end: ast::Expr,
5839 center: ast::Expr,
5840 direction: Option<ArcDirection>,
5841 construction: Option<bool>,
5842 },
5843 EditCircle {
5844 start: ast::Expr,
5845 center: ast::Expr,
5846 construction: Option<bool>,
5847 },
5848 EditControlPointSpline {
5849 points: ast::Expr,
5850 construction: Option<bool>,
5851 },
5852 EditConstraintValue {
5853 value: ast::BinaryPart,
5854 },
5855 EditAngleConstraint {
5856 call: ast::BinaryPart,
5857 value: ast::BinaryPart,
5858 },
5859 EditDistanceConstraint {
5860 call: ast::BinaryPart,
5861 value: ast::BinaryPart,
5862 },
5863 EditDistanceConstraintLabelPosition {
5864 label_position: ast::Expr,
5865 },
5866 EditCallUnlabeled {
5867 arg: ast::Expr,
5868 },
5869 EditVarInitialValue {
5870 value: Number,
5871 },
5872 DeleteNode,
5873}
5874
5875impl AstMutateCommand {
5876 fn needs_defined_names_stack(&self) -> bool {
5877 matches!(
5878 self,
5879 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5880 )
5881 }
5882}
5883
5884#[derive(Debug)]
5885enum AstMutateCommandReturn {
5886 None,
5887 Name(String),
5888}
5889
5890#[derive(Debug, Clone)]
5891struct AstNodeRef {
5892 range: SourceRange,
5893 node_path: Option<ast::NodePath>,
5894}
5895
5896impl<T> From<&ast::Node<T>> for AstNodeRef {
5897 fn from(value: &ast::Node<T>) -> Self {
5898 AstNodeRef {
5899 range: value.into(),
5900 node_path: value.node_path.clone(),
5901 }
5902 }
5903}
5904
5905impl From<&ast::BodyItem> for AstNodeRef {
5906 fn from(value: &ast::BodyItem) -> Self {
5907 match value {
5908 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5909 range: node.into(),
5910 node_path: node.node_path.clone(),
5911 },
5912 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5913 range: node.into(),
5914 node_path: node.node_path.clone(),
5915 },
5916 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5917 range: node.into(),
5918 node_path: node.node_path.clone(),
5919 },
5920 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5921 range: node.into(),
5922 node_path: node.node_path.clone(),
5923 },
5924 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5925 range: node.into(),
5926 node_path: node.node_path.clone(),
5927 },
5928 }
5929 }
5930}
5931
5932impl From<&ast::Expr> for AstNodeRef {
5933 fn from(value: &ast::Expr) -> Self {
5934 AstNodeRef {
5935 range: SourceRange::from(value),
5936 node_path: value.node_path().cloned(),
5937 }
5938 }
5939}
5940
5941impl From<&AstMutateContext> for AstNodeRef {
5942 fn from(value: &AstMutateContext) -> Self {
5943 AstNodeRef {
5944 range: value.source_range,
5945 node_path: value.node_path.clone(),
5946 }
5947 }
5948}
5949
5950impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5951 type Error = crate::walk::AstNodeError;
5952
5953 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5954 Ok(AstNodeRef {
5955 range: SourceRange::try_from(value)?,
5956 node_path: value.try_into()?,
5957 })
5958 }
5959}
5960
5961impl From<AstNodeRef> for SourceRange {
5962 fn from(value: AstNodeRef) -> Self {
5963 value.range
5964 }
5965}
5966
5967impl Visitor for AstMutateContext {
5968 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5969 type Continue = ();
5970
5971 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5972 filter_and_process(self, node)
5973 }
5974
5975 fn finish(&mut self, node: NodeMut<'_>) {
5976 match &node {
5977 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5978 self.defined_names_stack.pop();
5979 }
5980 _ => {}
5981 }
5982 }
5983}
5984
5985fn filter_and_process(
5986 ctx: &mut AstMutateContext,
5987 node: NodeMut,
5988) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5989 let Ok(node_range) = SourceRange::try_from(&node) else {
5990 return TraversalReturn::new_continue(());
5992 };
5993 if let NodeMut::VariableDeclaration(var_decl) = &node {
5998 let expr_range = SourceRange::from(&var_decl.declaration.init);
5999 let expr_node_path = var_decl.declaration.init.node_path();
6000 if source_ref_matches(ctx, expr_range, expr_node_path) {
6001 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6002 return TraversalReturn::new_break(Ok((
6005 AstNodeRef::from(&**var_decl),
6006 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6007 )));
6008 }
6009 if let AstMutateCommand::DeleteNode = &ctx.command {
6010 return TraversalReturn {
6013 mutate_body_item: MutateBodyItem::Delete,
6014 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6015 };
6016 }
6017 }
6018 }
6019 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6022 let expr_range = SourceRange::from(&expr_stmt.expression);
6023 let expr_node_path = expr_stmt.expression.node_path();
6024 if source_ref_matches(ctx, expr_range, expr_node_path) {
6025 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6026 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6029 return TraversalReturn::new_continue(());
6030 };
6031 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6032 }
6033 if let AstMutateCommand::DeleteNode = &ctx.command {
6034 return TraversalReturn {
6037 mutate_body_item: MutateBodyItem::Delete,
6038 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6039 };
6040 }
6041 }
6042 }
6043
6044 if ctx.command.needs_defined_names_stack() {
6045 if let NodeMut::Program(program) = &node {
6046 ctx.defined_names_stack.push(find_defined_names(*program));
6047 } else if let NodeMut::SketchBlock(block) = &node {
6048 ctx.defined_names_stack.push(find_defined_names(&block.body));
6049 }
6050 }
6051
6052 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6054 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6055 return TraversalReturn::new_continue(());
6056 }
6057 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6058 return TraversalReturn::new_continue(());
6059 };
6060 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6061}
6062
6063fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6064 match &ctx.node_path {
6065 Some(target) => Some(target) == node_path,
6066 None => node_range == ctx.source_range,
6067 }
6068}
6069
6070fn is_angle_constraint_call_name(name: &str) -> bool {
6071 matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6072}
6073
6074fn is_distance_constraint_call_name(name: &str) -> bool {
6075 matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6076}
6077
6078fn is_constraint_call_name(name: &str) -> bool {
6079 matches!(
6080 name,
6081 DISTANCE_FN
6082 | HORIZONTAL_DISTANCE_FN
6083 | VERTICAL_DISTANCE_FN
6084 | RADIUS_FN
6085 | DIAMETER_FN
6086 | ANGLE_FN
6087 | ANGLE_DIMENSION_FN
6088 )
6089}
6090
6091fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6092 if let ast::BinaryPart::CallExpressionKw(call) = part
6093 && is_constraint_call_name(call.callee.name.name.as_str())
6094 {
6095 Some(call)
6096 } else {
6097 None
6098 }
6099}
6100
6101fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6102 match &ctx.command {
6103 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6104 if let NodeMut::SketchBlock(sketch_block) = node {
6105 sketch_block
6106 .body
6107 .items
6108 .push(ast::BodyItem::ExpressionStatement(ast::Node {
6109 inner: ast::ExpressionStatement {
6110 expression: expr.clone(),
6111 digest: None,
6112 },
6113 start: Default::default(),
6114 end: Default::default(),
6115 module_id: Default::default(),
6116 node_path: None,
6117 outer_attrs: Default::default(),
6118 pre_comments: Default::default(),
6119 comment_start: Default::default(),
6120 }));
6121 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6122 }
6123 }
6124 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6125 if let NodeMut::SketchBlock(sketch_block) = node {
6126 let empty_defined_names = HashSet::new();
6127 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6128 let Ok(name) = next_free_name(prefix, defined_names) else {
6129 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6130 };
6131 sketch_block
6132 .body
6133 .items
6134 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6135 ast::VariableDeclaration::new(
6136 ast::VariableDeclarator::new(&name, expr.clone()),
6137 ast::ItemVisibility::Default,
6138 ast::VariableKind::Const,
6139 ),
6140 ))));
6141 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6142 }
6143 }
6144 AstMutateCommand::AddVariableDeclaration { prefix } => {
6145 if let NodeMut::VariableDeclaration(inner) = node {
6146 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6147 }
6148 if let NodeMut::ExpressionStatement(expr_stmt) = node {
6149 let empty_defined_names = HashSet::new();
6150 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6151 let Ok(name) = next_free_name(prefix, defined_names) else {
6152 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6154 };
6155 let mutate_node =
6156 ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6157 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6158 ast::ItemVisibility::Default,
6159 ast::VariableKind::Const,
6160 ))));
6161 return TraversalReturn {
6162 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6163 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6164 };
6165 }
6166 }
6167 AstMutateCommand::EditPoint { at } => {
6168 if let NodeMut::CallExpressionKw(call) = node {
6169 if call.callee.name.name != POINT_FN {
6170 return TraversalReturn::new_continue(());
6171 }
6172 for labeled_arg in &mut call.arguments {
6174 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6175 labeled_arg.arg = at.clone();
6176 }
6177 }
6178 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6179 }
6180 }
6181 AstMutateCommand::EditLine {
6182 start,
6183 end,
6184 construction,
6185 } => {
6186 if let NodeMut::CallExpressionKw(call) = node {
6187 if call.callee.name.name != LINE_FN {
6188 return TraversalReturn::new_continue(());
6189 }
6190 for labeled_arg in &mut call.arguments {
6192 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6193 labeled_arg.arg = start.clone();
6194 }
6195 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6196 labeled_arg.arg = end.clone();
6197 }
6198 }
6199 if let Some(construction_value) = construction {
6201 let construction_exists = call
6202 .arguments
6203 .iter()
6204 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6205 if *construction_value {
6206 if construction_exists {
6208 for labeled_arg in &mut call.arguments {
6210 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6211 labeled_arg.arg =
6212 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6213 value: ast::LiteralValue::Bool(true),
6214 raw: "true".to_string(),
6215 digest: None,
6216 })));
6217 }
6218 }
6219 } else {
6220 call.arguments.push(ast::LabeledArg {
6222 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6223 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6224 value: ast::LiteralValue::Bool(true),
6225 raw: "true".to_string(),
6226 digest: None,
6227 }))),
6228 });
6229 }
6230 } else {
6231 call.arguments
6233 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6234 }
6235 }
6236 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6237 }
6238 }
6239 AstMutateCommand::EditArc {
6240 start,
6241 end,
6242 center,
6243 direction,
6244 construction,
6245 } => {
6246 if let NodeMut::CallExpressionKw(call) = node {
6247 if call.callee.name.name != ARC_FN {
6248 return TraversalReturn::new_continue(());
6249 }
6250 for labeled_arg in &mut call.arguments {
6252 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6253 labeled_arg.arg = start.clone();
6254 }
6255 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6256 labeled_arg.arg = end.clone();
6257 }
6258 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6259 labeled_arg.arg = center.clone();
6260 }
6261 }
6262 if let Some(direction_value) = direction {
6264 let direction_exists = call
6265 .arguments
6266 .iter()
6267 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6268 if direction_value.is_clockwise() {
6269 let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6270 if direction_exists {
6271 for labeled_arg in &mut call.arguments {
6273 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6274 labeled_arg.arg = direction_ast.clone();
6275 }
6276 }
6277 } else {
6278 call.arguments.push(ast::LabeledArg {
6280 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6281 arg: direction_ast,
6282 });
6283 }
6284 } else {
6285 call.arguments
6288 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6289 }
6290 }
6291 if let Some(construction_value) = construction {
6293 let construction_exists = call
6294 .arguments
6295 .iter()
6296 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6297 if *construction_value {
6298 if construction_exists {
6300 for labeled_arg in &mut call.arguments {
6302 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6303 labeled_arg.arg =
6304 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6305 value: ast::LiteralValue::Bool(true),
6306 raw: "true".to_string(),
6307 digest: None,
6308 })));
6309 }
6310 }
6311 } else {
6312 call.arguments.push(ast::LabeledArg {
6314 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6315 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6316 value: ast::LiteralValue::Bool(true),
6317 raw: "true".to_string(),
6318 digest: None,
6319 }))),
6320 });
6321 }
6322 } else {
6323 call.arguments
6325 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6326 }
6327 }
6328 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6329 }
6330 }
6331 AstMutateCommand::EditCircle {
6332 start,
6333 center,
6334 construction,
6335 } => {
6336 if let NodeMut::CallExpressionKw(call) = node {
6337 if call.callee.name.name != CIRCLE_FN {
6338 return TraversalReturn::new_continue(());
6339 }
6340 for labeled_arg in &mut call.arguments {
6342 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6343 labeled_arg.arg = start.clone();
6344 }
6345 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6346 labeled_arg.arg = center.clone();
6347 }
6348 }
6349 if let Some(construction_value) = construction {
6351 let construction_exists = call
6352 .arguments
6353 .iter()
6354 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6355 if *construction_value {
6356 if construction_exists {
6357 for labeled_arg in &mut call.arguments {
6359 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6360 labeled_arg.arg =
6361 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6362 value: ast::LiteralValue::Bool(true),
6363 raw: "true".to_string(),
6364 digest: None,
6365 })));
6366 }
6367 }
6368 } else {
6369 call.arguments.push(ast::LabeledArg {
6371 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6372 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6373 value: ast::LiteralValue::Bool(true),
6374 raw: "true".to_string(),
6375 digest: None,
6376 }))),
6377 });
6378 }
6379 } else {
6380 call.arguments
6382 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6383 }
6384 }
6385 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6386 }
6387 }
6388 AstMutateCommand::EditControlPointSpline { points, construction } => {
6389 if let NodeMut::CallExpressionKw(call) = node {
6390 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6391 return TraversalReturn::new_continue(());
6392 }
6393 for labeled_arg in &mut call.arguments {
6394 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6395 {
6396 labeled_arg.arg = points.clone();
6397 }
6398 }
6399 if let Some(construction_value) = construction {
6401 let construction_exists = call
6402 .arguments
6403 .iter()
6404 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6405 if *construction_value {
6406 if construction_exists {
6407 for labeled_arg in &mut call.arguments {
6408 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6409 labeled_arg.arg =
6410 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6411 value: ast::LiteralValue::Bool(true),
6412 raw: "true".to_string(),
6413 digest: None,
6414 })));
6415 }
6416 }
6417 } else {
6418 call.arguments.push(ast::LabeledArg {
6419 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6420 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6421 value: ast::LiteralValue::Bool(true),
6422 raw: "true".to_string(),
6423 digest: None,
6424 }))),
6425 });
6426 }
6427 } else {
6428 call.arguments
6429 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6430 }
6431 }
6432 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6433 }
6434 }
6435 AstMutateCommand::EditConstraintValue { value } => {
6436 if let NodeMut::BinaryExpression(binary_expr) = node {
6437 let left_is_constraint = matches!(
6438 &binary_expr.left,
6439 ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6440 );
6441 if left_is_constraint {
6442 binary_expr.right = value.clone();
6443 } else {
6444 binary_expr.left = value.clone();
6445 }
6446
6447 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6448 }
6449 }
6450 AstMutateCommand::EditAngleConstraint { call, value } => {
6451 if let NodeMut::BinaryExpression(binary_expr) = node {
6452 let left_is_angle = matches!(
6453 &binary_expr.left,
6454 ast::BinaryPart::CallExpressionKw(existing_call)
6455 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6456 );
6457 let right_is_angle = matches!(
6458 &binary_expr.right,
6459 ast::BinaryPart::CallExpressionKw(existing_call)
6460 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6461 );
6462
6463 match (left_is_angle, right_is_angle) {
6464 (true, _) => {
6465 binary_expr.left = call.clone();
6466 binary_expr.right = value.clone();
6467 }
6468 (false, true) => {
6469 binary_expr.left = value.clone();
6470 binary_expr.right = call.clone();
6471 }
6472 (false, false) => return TraversalReturn::new_continue(()),
6473 }
6474
6475 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6476 }
6477 }
6478 AstMutateCommand::EditDistanceConstraint { call, value } => {
6479 if let NodeMut::BinaryExpression(binary_expr) = node {
6480 let left_is_distance = matches!(
6481 &binary_expr.left,
6482 ast::BinaryPart::CallExpressionKw(existing_call)
6483 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6484 );
6485 let right_is_distance = matches!(
6486 &binary_expr.right,
6487 ast::BinaryPart::CallExpressionKw(existing_call)
6488 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6489 );
6490
6491 match (left_is_distance, right_is_distance) {
6492 (true, _) => {
6493 binary_expr.left = call.clone();
6494 binary_expr.right = value.clone();
6495 }
6496 (false, true) => {
6497 binary_expr.left = value.clone();
6498 binary_expr.right = call.clone();
6499 }
6500 (false, false) => return TraversalReturn::new_continue(()),
6501 }
6502
6503 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6504 }
6505 }
6506 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6507 if let NodeMut::BinaryExpression(binary_expr) = node {
6508 let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6509 call
6510 } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6511 call
6512 } else {
6513 return TraversalReturn::new_continue(());
6514 };
6515
6516 if let Some(label_arg) = call
6517 .arguments
6518 .iter_mut()
6519 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6520 {
6521 label_arg.arg = label_position.clone();
6522 } else {
6523 call.arguments.push(ast::LabeledArg {
6524 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6525 arg: label_position.clone(),
6526 });
6527 }
6528
6529 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6530 }
6531 }
6532 AstMutateCommand::EditCallUnlabeled { arg } => {
6533 if let NodeMut::CallExpressionKw(call) = node {
6534 call.unlabeled = Some(arg.clone());
6535 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6536 }
6537 }
6538 AstMutateCommand::EditVarInitialValue { value } => {
6539 if let NodeMut::SketchVar(sketch_var) = node {
6543 let Ok(literal) = to_source_number(*value) else {
6544 return TraversalReturn::new_break(Err(KclError::refactor(format!(
6545 "Could not convert number to AST literal: {:?}",
6546 *value
6547 ))));
6548 };
6549 sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6550 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6551 }
6552 }
6553 AstMutateCommand::DeleteNode => {
6554 return TraversalReturn {
6555 mutate_body_item: MutateBodyItem::Delete,
6556 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6557 };
6558 }
6559 }
6560 TraversalReturn::new_continue(())
6561}
6562
6563struct FindSketchBlockSourceRange {
6564 target_before_mutation: SourceRange,
6566 found: Cell<Option<AstNodeRef>>,
6570}
6571
6572impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6573 type Error = crate::front::Error;
6574
6575 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6576 let Ok(node_range) = SourceRange::try_from(&node) else {
6577 return Ok(true);
6578 };
6579
6580 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6581 if node_range.module_id() == self.target_before_mutation.module_id()
6582 && node_range.start() == self.target_before_mutation.start()
6583 && node_range.end() >= self.target_before_mutation.end()
6585 {
6586 self.found.set(sketch_block.body.items.last().map(|item| match item {
6587 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6591 _ => AstNodeRef::from(item),
6592 }));
6593 return Ok(false);
6594 } else {
6595 return Ok(true);
6598 }
6599 }
6600
6601 for child in node.children().iter() {
6602 if !child.visit(*self)? {
6603 return Ok(false);
6604 }
6605 }
6606
6607 Ok(true)
6608 }
6609}
6610
6611struct FindSketchBlockByNodePath {
6612 target_node_path: ast::NodePath,
6614 found: Cell<Option<AstNodeRef>>,
6618}
6619
6620impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6621 type Error = crate::front::Error;
6622
6623 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6624 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6625 return Ok(true);
6626 };
6627
6628 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6629 if let Some(node_path) = node_path
6630 && node_path == self.target_node_path
6631 {
6632 self.found.set(sketch_block.body.items.last().map(|item| match item {
6633 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6637 _ => AstNodeRef::from(item),
6638 }));
6639
6640 return Ok(false);
6641 } else {
6642 return Ok(true);
6645 }
6646 }
6647
6648 for child in node.children().iter() {
6649 if !child.visit(*self)? {
6650 return Ok(false);
6651 }
6652 }
6653
6654 Ok(true)
6655 }
6656}
6657
6658fn find_sketch_block_added_item(
6666 ast: &ast::Node<ast::Program>,
6667 sketch_block_before_mutation: &AstNodeRef,
6668) -> Result<AstNodeRef, KclError> {
6669 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6670 let find = FindSketchBlockByNodePath {
6671 target_node_path: node_path.clone(),
6672 found: Cell::new(None),
6673 };
6674 let node = crate::walk::Node::from(ast);
6675 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6676 find.found.into_inner().ok_or_else(|| {
6677 KclError::refactor(format!(
6678 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6679 ))
6680 })
6681 } else {
6682 let find = FindSketchBlockSourceRange {
6684 target_before_mutation: sketch_block_before_mutation.range,
6685 found: Cell::new(None),
6686 };
6687 let node = crate::walk::Node::from(ast);
6688 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6689 find.found.into_inner().ok_or_else(|| KclError::refactor(
6690 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?"),
6691 ))
6692 }
6693}
6694
6695fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6696 let message = error.message().trim();
6697 let message = if message.is_empty() {
6698 "unknown parse error"
6699 } else {
6700 message
6701 };
6702
6703 format!("{prefix}: {message}")
6704}
6705
6706fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6707 let (program, errors) = Program::parse(source).map_err(|err| {
6708 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6709 })?;
6710 if !errors.is_empty() {
6711 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6712 format_compilation_issues(parse_error_prefix, &errors),
6713 )));
6714 }
6715
6716 program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6717}
6718
6719fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6720 let Some(first_issue) = issues
6721 .iter()
6722 .find(|issue| issue.severity.is_err())
6723 .or_else(|| issues.first())
6724 else {
6725 return prefix.to_owned();
6726 };
6727
6728 let message = first_issue.message.trim();
6729 let message = if message.is_empty() {
6730 "unknown parse error"
6731 } else {
6732 message
6733 };
6734
6735 if issues.len() > 1 {
6736 format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6737 } else {
6738 format!("{prefix}: {message}")
6739 }
6740}
6741
6742fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6743 ast.recast_top(&Default::default(), 0)
6745}
6746
6747struct FindNumericLiteral {
6748 target: SourceRange,
6749 found: Cell<Option<ast::NumericLiteral>>,
6750}
6751
6752impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6753 type Error = crate::front::Error;
6754
6755 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6756 let Ok(node_range) = SourceRange::try_from(&node) else {
6757 return Ok(true);
6758 };
6759
6760 if node_range == self.target
6761 && let crate::walk::Node::NumericLiteral(literal) = node
6762 {
6763 self.found.set(Some(literal.inner.clone()));
6764 return Ok(false);
6765 }
6766
6767 for child in node.children().iter() {
6768 if !child.visit(*self)? {
6769 return Ok(false);
6770 }
6771 }
6772
6773 Ok(true)
6774 }
6775}
6776
6777fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6778 let find = FindNumericLiteral {
6779 target,
6780 found: Cell::new(None),
6781 };
6782 let node = crate::walk::Node::from(ast);
6783 node.visit(&find).ok()?;
6784 find.found.into_inner()
6785}
6786
6787struct FindSketchVarInitialByNodePath<'a> {
6788 target: &'a ast::NodePath,
6789 sketch_var_found: Cell<bool>,
6790 initial_literal: Cell<Option<ast::NumericLiteral>>,
6791}
6792
6793impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6794 type Error = crate::front::Error;
6795
6796 fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6797 if let crate::walk::Node::SketchVar(sketch_var) = node
6798 && sketch_var.node_path.as_ref() == Some(self.target)
6799 {
6800 self.sketch_var_found.set(true);
6801 if let Some(initial) = &sketch_var.initial {
6802 self.initial_literal.set(Some(initial.inner.clone()));
6803 }
6804 return Ok(false);
6805 }
6806
6807 for child in node.children().iter() {
6808 if !child.visit(*self)? {
6809 return Ok(false);
6810 }
6811 }
6812
6813 Ok(true)
6814 }
6815}
6816
6817fn numeric_literal_at_node_path(
6827 ast: &ast::Node<ast::Program>,
6828 node_path: Option<&ast::NodePath>,
6829 source_range: SourceRange,
6830) -> Option<Option<ast::NumericLiteral>> {
6831 let Some(node_path) = node_path else {
6832 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";
6833 #[cfg(target_arch = "wasm32")]
6834 web_sys::console::warn_1(&message.into());
6835 #[cfg(not(target_arch = "wasm32"))]
6836 eprintln!("WARNING: {message}");
6837 return numeric_literal_at_source_range(ast, source_range).map(Some);
6838 };
6839 let find = FindSketchVarInitialByNodePath {
6840 target: node_path,
6841 sketch_var_found: Cell::new(false),
6842 initial_literal: Cell::new(None),
6843 };
6844 let node = crate::walk::Node::from(ast);
6845 node.visit(&find).ok()?;
6846 if !find.sketch_var_found.get() {
6847 return None;
6848 }
6849 Some(find.initial_literal.into_inner())
6850}
6851
6852fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6853 match suffix {
6854 NumericSuffix::Mm => Some(UnitLength::Millimeters),
6855 NumericSuffix::Cm => Some(UnitLength::Centimeters),
6856 NumericSuffix::M => Some(UnitLength::Meters),
6857 NumericSuffix::Inch => Some(UnitLength::Inches),
6858 NumericSuffix::Ft => Some(UnitLength::Feet),
6859 NumericSuffix::Yd => Some(UnitLength::Yards),
6860 _ => None,
6861 }
6862}
6863
6864fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6865 match suffix_length_unit(number.units) {
6866 Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6867 None => number.value,
6868 }
6869}
6870
6871fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6872 match suffix_length_unit(literal.suffix) {
6873 Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6874 None => literal.value,
6875 }
6876}
6877
6878fn var_solution_needs_commit(
6879 current_literal: &ast::NumericLiteral,
6880 solved_value: Number,
6881 default_length_unit: UnitLength,
6882) -> bool {
6883 let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6884 let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6885
6886 (current - solved).abs() > 1e-9
6887}
6888
6889fn preserve_var_solution_literal_style(
6890 current_literal: &ast::NumericLiteral,
6891 solved_value: Number,
6892 default_length_unit: UnitLength,
6893) -> Number {
6894 if current_literal.suffix == NumericSuffix::None {
6895 return Number {
6896 value: number_value_in_default_length_units(solved_value, default_length_unit),
6897 units: NumericSuffix::None,
6898 };
6899 }
6900
6901 let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6902 return solved_value;
6903 };
6904
6905 let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6906 Number {
6907 value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6908 units: current_literal.suffix,
6909 }
6910}
6911
6912pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6913 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6914 inner: ast::ArrayExpression {
6915 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6916 non_code_meta: Default::default(),
6917 digest: None,
6918 },
6919 start: Default::default(),
6920 end: Default::default(),
6921 module_id: Default::default(),
6922 node_path: None,
6923 outer_attrs: Default::default(),
6924 pre_comments: Default::default(),
6925 comment_start: Default::default(),
6926 })))
6927}
6928
6929pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6930 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6931 ast::ArrayExpression {
6932 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6933 digest: None,
6934 non_code_meta: Default::default(),
6935 },
6936 ))))
6937}
6938
6939fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6940 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6941 ast::ArrayExpression {
6942 elements: vec![
6943 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6944 point.x,
6945 )?)))),
6946 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6947 point.y,
6948 )?)))),
6949 ],
6950 non_code_meta: Default::default(),
6951 digest: None,
6952 },
6953 ))))
6954}
6955
6956fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6957 match expr {
6958 Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6959 inner: ast::Literal::from(to_source_number(*number)?),
6960 start: Default::default(),
6961 end: Default::default(),
6962 module_id: Default::default(),
6963 node_path: None,
6964 outer_attrs: Default::default(),
6965 pre_comments: Default::default(),
6966 comment_start: Default::default(),
6967 }))),
6968 Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6969 inner: ast::SketchVar {
6970 initial: Some(BoxNode::new(ast::Node {
6971 inner: to_source_number(*number)?,
6972 start: Default::default(),
6973 end: Default::default(),
6974 module_id: Default::default(),
6975 node_path: None,
6976 outer_attrs: Default::default(),
6977 pre_comments: Default::default(),
6978 comment_start: Default::default(),
6979 })),
6980 digest: None,
6981 },
6982 start: Default::default(),
6983 end: Default::default(),
6984 module_id: Default::default(),
6985 node_path: None,
6986 outer_attrs: Default::default(),
6987 pre_comments: Default::default(),
6988 comment_start: Default::default(),
6989 }))),
6990 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6991 }
6992}
6993
6994fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6995 Ok(ast::NumericLiteral {
6996 value: number.value,
6997 suffix: number.units,
6998 raw: format_number_literal(number.value, number.units, None)?,
6999 digest: None,
7000 })
7001}
7002
7003pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7004 ast::Expr::Name(BoxNode::new(ast_name(name)))
7005}
7006
7007fn ast_name(name: String) -> ast::Node<ast::Name> {
7008 ast::Node {
7009 inner: ast::Name {
7010 name: ast::Node {
7011 inner: ast::Identifier { name, digest: None },
7012 start: Default::default(),
7013 end: Default::default(),
7014 module_id: Default::default(),
7015 node_path: None,
7016 outer_attrs: Default::default(),
7017 pre_comments: Default::default(),
7018 comment_start: Default::default(),
7019 },
7020 path: Vec::new(),
7021 abs_path: false,
7022 digest: None,
7023 },
7024 start: Default::default(),
7025 end: Default::default(),
7026 module_id: Default::default(),
7027 node_path: None,
7028 outer_attrs: Default::default(),
7029 pre_comments: Default::default(),
7030 comment_start: Default::default(),
7031 }
7032}
7033
7034pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7035 ast::Name {
7036 name: ast::Node {
7037 inner: ast::Identifier {
7038 name: name.to_owned(),
7039 digest: None,
7040 },
7041 start: Default::default(),
7042 end: Default::default(),
7043 module_id: Default::default(),
7044 node_path: None,
7045 outer_attrs: Default::default(),
7046 pre_comments: Default::default(),
7047 comment_start: Default::default(),
7048 },
7049 path: Default::default(),
7050 abs_path: false,
7051 digest: None,
7052 }
7053}
7054
7055pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
7057 let elements = exprs.into_iter().collect::<Vec<_>>();
7058 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
7059
7060 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7062 elements,
7063 digest: None,
7064 non_code_meta: Default::default(),
7065 })));
7066
7067 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7069 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
7070 unlabeled: Some(array_expr),
7071 arguments: Default::default(),
7072 digest: None,
7073 non_code_meta: Default::default(),
7074 })))
7075}
7076
7077pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7079 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7080 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7081 unlabeled: Some(line_expr),
7082 arguments: Default::default(),
7083 digest: None,
7084 non_code_meta: Default::default(),
7085 })))
7086}
7087
7088pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7090 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7091 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7092 unlabeled: Some(line_expr),
7093 arguments: Default::default(),
7094 digest: None,
7095 non_code_meta: Default::default(),
7096 })))
7097}
7098
7099pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7101 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7102 object: object_expr,
7103 property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7104 name: ast::Node::no_src(ast::Identifier {
7105 name: property.to_string(),
7106 digest: None,
7107 }),
7108 path: Vec::new(),
7109 abs_path: false,
7110 digest: None,
7111 }))),
7112 computed: false,
7113 digest: None,
7114 })))
7115}
7116
7117pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7118 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7119 object: object_expr,
7120 property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7121 ast::NumericLiteral {
7122 value: index as f64,
7123 suffix: NumericSuffix::None,
7124 raw: index.to_string(),
7125 digest: None,
7126 },
7127 )))),
7128 computed: true,
7129 digest: None,
7130 })))
7131}
7132
7133fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7135 let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7137 position.x,
7138 )?))));
7139 let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7140 position.y,
7141 )?))));
7142 let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7143 elements: vec![x_literal, y_literal],
7144 digest: None,
7145 non_code_meta: Default::default(),
7146 })));
7147
7148 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7150 elements: vec![point_expr, point_array],
7151 digest: None,
7152 non_code_meta: Default::default(),
7153 })));
7154
7155 Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7157 ast::CallExpressionKw {
7158 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7159 unlabeled: Some(array_expr),
7160 arguments: Default::default(),
7161 digest: None,
7162 non_code_meta: Default::default(),
7163 },
7164 ))))
7165}
7166
7167pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7169 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7170 elements: line_exprs,
7171 digest: None,
7172 non_code_meta: Default::default(),
7173 })));
7174
7175 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7177 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7178 unlabeled: Some(array_expr),
7179 arguments: Default::default(),
7180 digest: None,
7181 non_code_meta: Default::default(),
7182 })))
7183}
7184
7185pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7187 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7188 elements: segment_exprs,
7189 digest: None,
7190 non_code_meta: Default::default(),
7191 })));
7192
7193 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7194 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7195 unlabeled: Some(array_expr),
7196 arguments: Default::default(),
7197 digest: None,
7198 non_code_meta: Default::default(),
7199 })))
7200}
7201
7202pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7204 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7205 elements: vec![seg1_expr, seg2_expr],
7206 digest: None,
7207 non_code_meta: Default::default(),
7208 })));
7209
7210 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7211 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7212 unlabeled: Some(array_expr),
7213 arguments: Default::default(),
7214 digest: None,
7215 non_code_meta: Default::default(),
7216 })))
7217}
7218
7219pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7221 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7222 elements: input_exprs,
7223 digest: None,
7224 non_code_meta: Default::default(),
7225 })));
7226 let arguments = vec![ast::LabeledArg {
7227 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7228 arg: axis_expr,
7229 }];
7230
7231 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7232 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7233 unlabeled: Some(array_expr),
7234 arguments,
7235 digest: None,
7236 non_code_meta: Default::default(),
7237 })))
7238}
7239
7240pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7242 let arguments = vec![ast::LabeledArg {
7243 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7244 arg: point_expr,
7245 }];
7246
7247 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7248 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7249 unlabeled: Some(segment_expr),
7250 arguments,
7251 digest: None,
7252 non_code_meta: Default::default(),
7253 })))
7254}
7255
7256fn issue_source_range(error: &KclError) -> SourceRange {
7261 let source_ranges = error.source_ranges();
7262 source_ranges
7263 .iter()
7264 .find(|range| range.is_top_level_module())
7265 .or_else(|| source_ranges.first())
7266 .copied()
7267 .unwrap_or_else(SourceRange::synthetic)
7268}
7269
7270#[cfg(test)]
7271mod tests {
7272 use std::sync;
7273
7274 use super::*;
7275 use crate::engine::PlaneName;
7276 use crate::engine::engine_manager::EngineManager;
7277 use crate::execution::cache::SketchModeState;
7278 use crate::execution::cache::clear_mem_cache;
7279 use crate::execution::cache::read_old_memory;
7280 use crate::execution::cache::write_old_memory;
7281 use crate::front::Distance;
7282 use crate::front::Fixed;
7283 use crate::front::FixedPoint;
7284 use crate::front::Midpoint;
7285 use crate::front::Object;
7286 use crate::front::Plane;
7287 use crate::front::Sketch;
7288 use crate::front::Tangent;
7289 use crate::frontend::sketch::Vertical;
7290 use crate::pretty::NumericSuffix;
7291
7292 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7293 for object in &scene_graph.objects {
7294 if let ObjectKind::Sketch(_) = &object.kind {
7295 return Some(object);
7296 }
7297 }
7298 None
7299 }
7300
7301 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7302 for object in &scene_graph.objects {
7303 if let ObjectKind::Face(_) = &object.kind {
7304 return Some(object);
7305 }
7306 }
7307 None
7308 }
7309
7310 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7311 for object in &scene_graph.objects {
7312 if matches!(&object.kind, ObjectKind::Wall(_)) {
7313 return Some(object.id);
7314 }
7315 }
7316 None
7317 }
7318
7319 fn find_cap_object_id_with_solid_output_index(
7320 scene_graph: &SceneGraph,
7321 cap_kind: crate::frontend::api::CapKind,
7322 solid_output_index: usize,
7323 ) -> Option<ObjectId> {
7324 for object in &scene_graph.objects {
7325 if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7326 {
7327 return Some(object.id);
7328 }
7329 }
7330 None
7331 }
7332
7333 #[test]
7334 fn issue_source_range_prefers_top_level_module() {
7335 use kcl_error::ModuleId;
7336
7337 let top = SourceRange::new(10, 20, ModuleId::default());
7338 let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7339
7340 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7343 "boom".to_owned(),
7344 vec![imported, top],
7345 ));
7346 assert_eq!(super::issue_source_range(&error), top);
7347
7348 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7350 assert_eq!(super::issue_source_range(&error), imported);
7351
7352 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7354 assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7355 }
7356
7357 #[test]
7358 fn composite_constituent_sweeps_are_not_solid_outputs() {
7359 use kcl_api::artifact::ArtifactSweepMethod;
7360 use kcl_api::artifact::CompositeSolid;
7361 use kcl_api::artifact::CompositeSolidSubType;
7362 use kcl_api::artifact::Sweep;
7363 use kcl_api::artifact::SweepSubType;
7364
7365 let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7366 let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7367 let composite_id = ArtifactId::new(Uuid::new_v4());
7368 let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7369 let sweep = |id| {
7370 Artifact::Sweep(Sweep {
7371 id,
7372 sub_type: SweepSubType::Extrusion,
7373 path_id: ArtifactId::new(Uuid::new_v4()),
7374 surface_ids: Vec::new(),
7375 edge_ids: Vec::new(),
7376 code_ref: code_ref.clone(),
7377 source_sweep_id: None,
7378 trajectory_id: None,
7379 method: ArtifactSweepMethod::New,
7380 consumed: false,
7381 pattern_ids: Vec::new(),
7382 })
7383 };
7384 let mut artifacts = IndexMap::from([
7385 (first_sweep_id, sweep(first_sweep_id)),
7386 (second_sweep_id, sweep(second_sweep_id)),
7387 ]);
7388
7389 let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7390 assert_eq!(
7391 solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7392 Some(0)
7393 );
7394 assert_eq!(
7395 solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7396 Some(1)
7397 );
7398
7399 artifacts.insert(
7400 composite_id,
7401 Artifact::CompositeSolid(CompositeSolid {
7402 id: composite_id,
7403 consumed: false,
7404 sub_type: CompositeSolidSubType::Union,
7405 output_index: None,
7406 solid_ids: vec![first_sweep_id, second_sweep_id],
7407 tool_ids: Vec::new(),
7408 code_ref,
7409 composite_solid_id: None,
7410 pattern_ids: Vec::new(),
7411 }),
7412 );
7413 let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7414 assert_eq!(
7415 solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7416 None
7417 );
7418 assert_eq!(
7419 solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7420 None
7421 );
7422 }
7423
7424 #[test]
7425 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7426 let source = "\
7427region001 = region(point = [0.1, 0.1], sketch = s)
7428extrude001 = extrude(region001, length = 5)
7429revolve001 = revolve(region001, axis = Y)
7430sweep001 = sweep(region001, path = path001)
7431loft001 = loft(region001)
7432not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7433";
7434
7435 let program = Program::parse(source).unwrap().0.unwrap();
7436
7437 assert_eq!(
7438 region_name_from_sweep_variable(&program.ast, "extrude001"),
7439 Some("region001".to_owned())
7440 );
7441 assert_eq!(
7442 region_name_from_sweep_variable(&program.ast, "revolve001"),
7443 Some("region001".to_owned())
7444 );
7445 assert_eq!(
7446 region_name_from_sweep_variable(&program.ast, "sweep001"),
7447 Some("region001".to_owned())
7448 );
7449 assert_eq!(
7450 region_name_from_sweep_variable(&program.ast, "loft001"),
7451 Some("region001".to_owned())
7452 );
7453 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7454 }
7455
7456 #[track_caller]
7457 fn expect_sketch(object: &Object) -> &Sketch {
7458 if let ObjectKind::Sketch(sketch) = &object.kind {
7459 sketch
7460 } else {
7461 panic!("Object is not a sketch: {:?}", object);
7462 }
7463 }
7464
7465 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7466 let point_object = scene_graph.objects.get(point_id.0).unwrap();
7467 let ObjectKind::Segment {
7468 segment: Segment::Point(point),
7469 } = &point_object.kind
7470 else {
7471 panic!("Object is not a point segment: {point_object:?}");
7472 };
7473 point.position.clone()
7474 }
7475
7476 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7477 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7478 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7479 }
7480
7481 fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7484 Point2d {
7485 x: Expr::Var(Number {
7486 value: x,
7487 units: NumericSuffix::Mm,
7488 }),
7489 y: Expr::Var(Number {
7490 value: y,
7491 units: NumericSuffix::Mm,
7492 }),
7493 }
7494 }
7495
7496 fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7499 Point2d {
7500 x: Number {
7501 value: x,
7502 units: NumericSuffix::Mm,
7503 },
7504 y: Number {
7505 value: y,
7506 units: NumericSuffix::Mm,
7507 },
7508 }
7509 }
7510
7511 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7512 LineCtor {
7513 start: Point2d {
7514 x: Expr::Number(Number { value: start_x, units }),
7515 y: Expr::Number(Number { value: start_y, units }),
7516 },
7517 end: Point2d {
7518 x: Expr::Number(Number { value: end_x, units }),
7519 y: Expr::Number(Number { value: end_y, units }),
7520 },
7521 construction: None,
7522 }
7523 }
7524
7525 async fn create_sketch_with_single_line(
7526 frontend: &mut FrontendState,
7527 ctx: &ExecutorContext,
7528 mock_ctx: &ExecutorContext,
7529 version: Version,
7530 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7531 frontend.program = Program::empty();
7532
7533 let sketch_args = SketchCtor {
7534 on: Plane::Default(PlaneName::Xy),
7535 };
7536 let (_src_delta, _scene_delta, sketch_id) = frontend
7537 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7538 .await
7539 .unwrap();
7540
7541 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7542 let (source_delta, scene_graph_delta) = frontend
7543 .add_segment(mock_ctx, version, sketch_id, segment, None)
7544 .await
7545 .unwrap();
7546 let line_id = *scene_graph_delta
7547 .new_objects
7548 .last()
7549 .expect("Expected line object id to be created");
7550
7551 (sketch_id, line_id, source_delta, scene_graph_delta)
7552 }
7553
7554 async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7555 frontend.program = program.clone();
7556 let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7557 frontend.update_state_after_exec(outcome, true);
7558 }
7559
7560 #[test]
7561 fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7562 for (source, expected_message) in [
7563 ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7564 ("3'", "Error parsing KCL source after editing: found unknown token '''"),
7565 ] {
7566 let err = parse_frontend_mutation_source(
7567 source,
7568 "Error parsing KCL source after editing",
7569 "No AST produced after editing",
7570 )
7571 .expect_err("expected invalid KCL source to fail");
7572 let message = err.error.message();
7573
7574 assert_eq!(message, expected_message);
7575 assert!(!message.contains("CompilationIssue"));
7576 assert!(!message.contains("KclErrorDetails"));
7577 assert!(!message.contains("source_range"));
7578 }
7579 }
7580
7581 #[tokio::test(flavor = "multi_thread")]
7582 async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7583 let initial_source = "\
7584sketch(on = XY) {
7585 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7586 distance([line1.start, line1.end]) == 10
7587}
7588";
7589 let program = Program::parse(initial_source).unwrap().0.unwrap();
7590
7591 let mut frontend = FrontendState::new();
7592 let mock_ctx = ExecutorContext::new_mock(None).await;
7593 let version = Version(0);
7594
7595 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7596 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7597 let sketch_id = sketch_object.id;
7598 let sketch = expect_sketch(sketch_object);
7599 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7600
7601 for (value, expected_message) in [
7602 ("**", "Invalid constraint value: Unexpected token: *"),
7603 ("3'", "Invalid constraint value: found unknown token '''"),
7604 ] {
7605 let err = frontend
7606 .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7607 .await
7608 .expect_err("expected invalid constraint expression to fail");
7609 let message = err.error.message();
7610
7611 assert_eq!(message, expected_message);
7612 assert!(!message.contains("CompilationIssue"));
7613 assert!(!message.contains("KclErrorDetails"));
7614 assert!(!message.contains("source_range"));
7615 }
7616
7617 mock_ctx.close().await;
7618 }
7619
7620 #[tokio::test(flavor = "multi_thread")]
7621 async fn test_failed_edit_constraint_value_does_not_update_program() {
7622 let initial_source = "\
7623sketch(on = XY) {
7624 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7625 distance([line1.start, line1.end]) == 10
7626}
7627";
7628 let program = Program::parse(initial_source).unwrap().0.unwrap();
7629 let original_source = program.original_file_contents.clone();
7630
7631 let mut frontend = FrontendState::new();
7632 let mock_ctx = ExecutorContext::new_mock(None).await;
7633 let version = Version(0);
7634
7635 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7636 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7637 let sketch_id = sketch_object.id;
7638 let sketch = expect_sketch(sketch_object);
7639 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7640
7641 frontend
7642 .edit_constraint_value(
7643 &mock_ctx,
7644 version,
7645 sketch_id,
7646 constraint_id,
7647 "unknownDistance".to_owned(),
7648 )
7649 .await
7650 .expect_err("expected invalid constraint value to fail execution");
7651
7652 assert_eq!(frontend.program.original_file_contents, original_source);
7653 assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7654
7655 mock_ctx.close().await;
7656 }
7657
7658 #[tokio::test(flavor = "multi_thread")]
7659 async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7660 let initial_source = "\
7661arr = [0]
7662sketch(on = XY) {
7663 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7664 distance([line1.start, line1.end]) == 10
7665}
7666";
7667 let program = Program::parse(initial_source).unwrap().0.unwrap();
7668
7669 let mut frontend = FrontendState::new();
7670 let mock_ctx = ExecutorContext::new_mock(None).await;
7671 let version = Version(0);
7672
7673 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7674 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7675 let sketch_id = sketch_object.id;
7676 let sketch = expect_sketch(sketch_object);
7677 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7678
7679 let err = frontend
7683 .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7684 .await
7685 .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7686 let message = err.error.message();
7687 assert!(
7688 message.contains("The array doesn't have any item at index 5"),
7689 "unexpected error message: {message}"
7690 );
7691
7692 mock_ctx.close().await;
7693 }
7694
7695 #[tokio::test(flavor = "multi_thread")]
7696 async fn test_sketch_checkpoint_round_trip_restores_state() {
7697 let mut frontend = FrontendState::new();
7698 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7699 let mock_ctx = ExecutorContext::new_mock(None).await;
7700 let version = Version(0);
7701
7702 let (sketch_id, line_id, source_delta, scene_graph_delta) =
7703 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7704
7705 let expected_source = source_delta.text.clone();
7706 let expected_scene_graph = frontend.scene_graph.clone();
7707 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7708 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7709
7710 let checkpoint_id = frontend
7711 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7712 .await
7713 .unwrap();
7714
7715 let edited_segments = vec![ExistingSegmentCtor {
7716 id: line_id,
7717 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7718 }];
7719 let (edited_source, _edited_scene) = frontend
7720 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7721 .await
7722 .unwrap();
7723 assert_ne!(edited_source.text, expected_source);
7724
7725 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7726
7727 assert_eq!(restored.source_delta.text, expected_source);
7728 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7729 assert!(restored.scene_graph_delta.invalidates_ids);
7730 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7731 assert_eq!(frontend.scene_graph, expected_scene_graph);
7732 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7733
7734 ctx.close().await;
7735 }
7736
7737 #[tokio::test(flavor = "multi_thread")]
7738 async fn test_sketch_checkpoints_prune_oldest_entries() {
7739 let mut frontend = FrontendState::new();
7740 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7741 let mock_ctx = ExecutorContext::new_mock(None).await;
7742 let version = Version(0);
7743
7744 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7745 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7746
7747 let mut checkpoint_ids = Vec::new();
7748 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7749 checkpoint_ids.push(
7750 frontend
7751 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7752 .await
7753 .unwrap(),
7754 );
7755 }
7756
7757 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7758 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7759
7760 let oldest_retained = checkpoint_ids[3];
7761 assert_eq!(
7762 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7763 Some(oldest_retained)
7764 );
7765
7766 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7767 assert!(evicted_restore.is_err());
7768 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7769
7770 frontend
7771 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7772 .await
7773 .unwrap();
7774
7775 ctx.close().await;
7776 }
7777
7778 #[tokio::test(flavor = "multi_thread")]
7779 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7780 let mut frontend = FrontendState::new();
7781 let missing_checkpoint = SketchCheckpointId::new(999);
7782
7783 let err = frontend
7784 .restore_sketch_checkpoint(missing_checkpoint)
7785 .await
7786 .expect_err("Expected restore to fail for missing checkpoint");
7787
7788 assert!(err.msg.contains("Sketch checkpoint not found"));
7789 }
7790
7791 #[tokio::test(flavor = "multi_thread")]
7792 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7793 let mut frontend = FrontendState::new();
7794 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7795 let mock_ctx = ExecutorContext::new_mock(None).await;
7796 let version = Version(0);
7797
7798 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7799 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7800
7801 let checkpoint_a = frontend
7802 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7803 .await
7804 .unwrap();
7805 let checkpoint_b = frontend
7806 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7807 .await
7808 .unwrap();
7809 assert_eq!(frontend.sketch_checkpoints.len(), 2);
7810
7811 frontend.clear_sketch_checkpoints();
7812 assert!(frontend.sketch_checkpoints.is_empty());
7813 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7814 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7815
7816 ctx.close().await;
7817 }
7818
7819 #[tokio::test(flavor = "multi_thread")]
7820 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7821 let mut frontend = FrontendState::new();
7822 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7823 let mock_ctx = ExecutorContext::new_mock(None).await;
7824 let version = Version(0);
7825
7826 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7827 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7828 let old_source = source_delta.text.clone();
7829 let old_checkpoint = frontend
7830 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7831 .await
7832 .unwrap();
7833 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7834
7835 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
7836 .unwrap()
7837 .0
7838 .unwrap();
7839
7840 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7841 let SetProgramOutcome::Success {
7842 checkpoint_id: Some(new_checkpoint),
7843 ..
7844 } = result
7845 else {
7846 panic!("Expected Success with a fresh checkpoint baseline");
7847 };
7848
7849 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7850
7851 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7852 assert_eq!(old_restore.source_delta.text, old_source);
7853
7854 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7855 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7856
7857 ctx.close().await;
7858 }
7859
7860 #[tokio::test(flavor = "multi_thread")]
7861 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7862 let mut frontend = FrontendState::new();
7863 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7864 let mock_ctx = ExecutorContext::new_mock(None).await;
7865 let version = Version(0);
7866
7867 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7868 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7869 let old_checkpoint = frontend
7870 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7871 .await
7872 .unwrap();
7873 let checkpoint_count_before = frontend.sketch_checkpoints.len();
7874
7875 let failing_program = Program::parse(
7876 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7877 )
7878 .unwrap()
7879 .0
7880 .unwrap();
7881
7882 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7883 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7884 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7885 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7886
7887 ctx.close().await;
7888 }
7889
7890 #[tokio::test(flavor = "multi_thread")]
7891 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7892 let mut frontend = FrontendState::new();
7893 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7894
7895 let program = Program::parse(
7896 "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",
7897 )
7898 .unwrap()
7899 .0
7900 .unwrap();
7901 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7902 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7903 panic!("Expected successful baseline program execution");
7904 };
7905
7906 clear_mem_cache().await;
7907 assert!(read_old_memory().await.is_none());
7908
7909 let checkpoint_without_mock_memory = frontend
7910 .create_sketch_checkpoint((*exec_outcome).clone())
7911 .await
7912 .unwrap();
7913
7914 write_old_memory(SketchModeState::new_for_tests()).await;
7915 assert!(read_old_memory().await.is_some());
7916
7917 let checkpoint_with_mock_memory = frontend
7918 .create_sketch_checkpoint((*exec_outcome).clone())
7919 .await
7920 .unwrap();
7921
7922 clear_mem_cache().await;
7923 assert!(read_old_memory().await.is_none());
7924
7925 frontend
7926 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7927 .await
7928 .unwrap();
7929 assert!(read_old_memory().await.is_some());
7930
7931 frontend
7932 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7933 .await
7934 .unwrap();
7935 assert!(read_old_memory().await.is_none());
7936
7937 ctx.close().await;
7938 }
7939
7940 #[tokio::test(flavor = "multi_thread")]
7941 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7942 let source = "\
7943sketch(on = XY) {
7944 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7945}
7946
7947bad = missing_name
7948";
7949 let program = Program::parse(source).unwrap().0.unwrap();
7950
7951 let mut frontend = FrontendState::new();
7952
7953 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7954 let mock_ctx = ExecutorContext::new_mock(None).await;
7955 let version = Version(0);
7956 let project_id = ProjectId(0);
7957 let file_id = FileId(0);
7958
7959 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7960 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7961 };
7962
7963 let sketch_id = frontend
7964 .scene_graph
7965 .objects
7966 .iter()
7967 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7968 .expect("Expected sketch object from errored hack_set_program");
7969
7970 frontend
7971 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7972 .await
7973 .unwrap();
7974
7975 ctx.close().await;
7976 mock_ctx.close().await;
7977 }
7978
7979 #[tokio::test(flavor = "multi_thread")]
7980 async fn test_new_sketch_add_point_edit_point() {
7981 let program = Program::empty();
7982
7983 let mut frontend = FrontendState::new();
7984 frontend.program = program;
7985
7986 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7987 let mock_ctx = ExecutorContext::new_mock(None).await;
7988 let version = Version(0);
7989
7990 let sketch_args = SketchCtor {
7991 on: Plane::Default(PlaneName::Xy),
7992 };
7993 let (_src_delta, scene_delta, sketch_id) = frontend
7994 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7995 .await
7996 .unwrap();
7997 assert_eq!(sketch_id, ObjectId(1));
7998 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7999 let sketch_object = &scene_delta.new_graph.objects[1];
8000 assert_eq!(sketch_object.id, ObjectId(1));
8001 assert_eq!(
8002 sketch_object.kind,
8003 ObjectKind::Sketch(Sketch {
8004 args: SketchCtor {
8005 on: Plane::Default(PlaneName::Xy)
8006 },
8007 plane: ObjectId(0),
8008 segments: vec![],
8009 constraints: vec![],
8010 })
8011 );
8012 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8013
8014 let point_ctor = PointCtor {
8015 position: Point2d {
8016 x: Expr::Number(Number {
8017 value: 1.0,
8018 units: NumericSuffix::Inch,
8019 }),
8020 y: Expr::Number(Number {
8021 value: 2.0,
8022 units: NumericSuffix::Inch,
8023 }),
8024 },
8025 };
8026 let segment = SegmentCtor::Point(point_ctor);
8027 let (src_delta, scene_delta) = frontend
8028 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8029 .await
8030 .unwrap();
8031 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8032 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8033 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8034 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8035 assert_eq!(scene_object.id.0, i);
8036 }
8037
8038 let point_id = *scene_delta.new_objects.last().unwrap();
8039
8040 let point_ctor = PointCtor {
8041 position: Point2d {
8042 x: Expr::Number(Number {
8043 value: 3.0,
8044 units: NumericSuffix::Inch,
8045 }),
8046 y: Expr::Number(Number {
8047 value: 4.0,
8048 units: NumericSuffix::Inch,
8049 }),
8050 },
8051 };
8052 let segments = vec![ExistingSegmentCtor {
8053 id: point_id,
8054 ctor: SegmentCtor::Point(point_ctor),
8055 }];
8056 let (src_delta, scene_delta) = frontend
8057 .edit_segments(&mock_ctx, version, sketch_id, segments)
8058 .await
8059 .unwrap();
8060 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8061 assert_eq!(scene_delta.new_objects, vec![]);
8062 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8063
8064 ctx.close().await;
8065 mock_ctx.close().await;
8066 }
8067
8068 #[tokio::test(flavor = "multi_thread")]
8069 async fn test_new_sketch_add_line_edit_line() {
8070 let program = Program::empty();
8071
8072 let mut frontend = FrontendState::new();
8073 frontend.program = program;
8074
8075 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8076 let mock_ctx = ExecutorContext::new_mock(None).await;
8077 let version = Version(0);
8078
8079 let sketch_args = SketchCtor {
8080 on: Plane::Default(PlaneName::Xy),
8081 };
8082 let (_src_delta, scene_delta, sketch_id) = frontend
8083 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8084 .await
8085 .unwrap();
8086 assert_eq!(sketch_id, ObjectId(1));
8087 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8088 let sketch_object = &scene_delta.new_graph.objects[1];
8089 assert_eq!(sketch_object.id, ObjectId(1));
8090 assert_eq!(
8091 sketch_object.kind,
8092 ObjectKind::Sketch(Sketch {
8093 args: SketchCtor {
8094 on: Plane::Default(PlaneName::Xy)
8095 },
8096 plane: ObjectId(0),
8097 segments: vec![],
8098 constraints: vec![],
8099 })
8100 );
8101 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8102
8103 let line_ctor = LineCtor {
8104 start: Point2d {
8105 x: Expr::Number(Number {
8106 value: 0.0,
8107 units: NumericSuffix::Mm,
8108 }),
8109 y: Expr::Number(Number {
8110 value: 0.0,
8111 units: NumericSuffix::Mm,
8112 }),
8113 },
8114 end: Point2d {
8115 x: Expr::Number(Number {
8116 value: 10.0,
8117 units: NumericSuffix::Mm,
8118 }),
8119 y: Expr::Number(Number {
8120 value: 10.0,
8121 units: NumericSuffix::Mm,
8122 }),
8123 },
8124 construction: None,
8125 };
8126 let segment = SegmentCtor::Line(line_ctor);
8127 let (src_delta, scene_delta) = frontend
8128 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8129 .await
8130 .unwrap();
8131 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8132 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8133 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8134 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8135 assert_eq!(scene_object.id.0, i);
8136 }
8137
8138 let line = *scene_delta.new_objects.last().unwrap();
8140
8141 let line_ctor = LineCtor {
8142 start: Point2d {
8143 x: Expr::Number(Number {
8144 value: 1.0,
8145 units: NumericSuffix::Mm,
8146 }),
8147 y: Expr::Number(Number {
8148 value: 2.0,
8149 units: NumericSuffix::Mm,
8150 }),
8151 },
8152 end: Point2d {
8153 x: Expr::Number(Number {
8154 value: 13.0,
8155 units: NumericSuffix::Mm,
8156 }),
8157 y: Expr::Number(Number {
8158 value: 14.0,
8159 units: NumericSuffix::Mm,
8160 }),
8161 },
8162 construction: None,
8163 };
8164 let segments = vec![ExistingSegmentCtor {
8165 id: line,
8166 ctor: SegmentCtor::Line(line_ctor),
8167 }];
8168 let (src_delta, scene_delta) = frontend
8169 .edit_segments(&mock_ctx, version, sketch_id, segments)
8170 .await
8171 .unwrap();
8172 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8173 assert_eq!(scene_delta.new_objects, vec![]);
8174 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8175
8176 ctx.close().await;
8177 mock_ctx.close().await;
8178 }
8179
8180 #[tokio::test(flavor = "multi_thread")]
8181 async fn test_new_sketch_add_arc_edit_arc() {
8182 let program = Program::empty();
8183
8184 let mut frontend = FrontendState::new();
8185 frontend.program = program;
8186
8187 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8188 let mock_ctx = ExecutorContext::new_mock(None).await;
8189 let version = Version(0);
8190
8191 let sketch_args = SketchCtor {
8192 on: Plane::Default(PlaneName::Xy),
8193 };
8194 let (_src_delta, scene_delta, sketch_id) = frontend
8195 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8196 .await
8197 .unwrap();
8198 assert_eq!(sketch_id, ObjectId(1));
8199 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8200 let sketch_object = &scene_delta.new_graph.objects[1];
8201 assert_eq!(sketch_object.id, ObjectId(1));
8202 assert_eq!(
8203 sketch_object.kind,
8204 ObjectKind::Sketch(Sketch {
8205 args: SketchCtor {
8206 on: Plane::Default(PlaneName::Xy),
8207 },
8208 plane: ObjectId(0),
8209 segments: vec![],
8210 constraints: vec![],
8211 })
8212 );
8213 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8214
8215 let arc_ctor = ArcCtor {
8216 start: Point2d {
8217 x: Expr::Var(Number {
8218 value: 0.0,
8219 units: NumericSuffix::Mm,
8220 }),
8221 y: Expr::Var(Number {
8222 value: 0.0,
8223 units: NumericSuffix::Mm,
8224 }),
8225 },
8226 end: Point2d {
8227 x: Expr::Var(Number {
8228 value: 10.0,
8229 units: NumericSuffix::Mm,
8230 }),
8231 y: Expr::Var(Number {
8232 value: 10.0,
8233 units: NumericSuffix::Mm,
8234 }),
8235 },
8236 center: Point2d {
8237 x: Expr::Var(Number {
8238 value: 10.0,
8239 units: NumericSuffix::Mm,
8240 }),
8241 y: Expr::Var(Number {
8242 value: 0.0,
8243 units: NumericSuffix::Mm,
8244 }),
8245 },
8246 direction: None,
8247 construction: None,
8248 };
8249 let segment = SegmentCtor::Arc(arc_ctor);
8250 let (src_delta, scene_delta) = frontend
8251 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8252 .await
8253 .unwrap();
8254 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8255 assert_eq!(
8256 scene_delta.new_objects,
8257 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8258 );
8259 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8260 assert_eq!(scene_object.id.0, i);
8261 }
8262 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8263
8264 let arc = *scene_delta.new_objects.last().unwrap();
8266
8267 let arc_ctor = ArcCtor {
8268 start: Point2d {
8269 x: Expr::Var(Number {
8270 value: 1.0,
8271 units: NumericSuffix::Mm,
8272 }),
8273 y: Expr::Var(Number {
8274 value: 2.0,
8275 units: NumericSuffix::Mm,
8276 }),
8277 },
8278 end: Point2d {
8279 x: Expr::Var(Number {
8280 value: 13.0,
8281 units: NumericSuffix::Mm,
8282 }),
8283 y: Expr::Var(Number {
8284 value: 14.0,
8285 units: NumericSuffix::Mm,
8286 }),
8287 },
8288 center: Point2d {
8289 x: Expr::Var(Number {
8290 value: 13.0,
8291 units: NumericSuffix::Mm,
8292 }),
8293 y: Expr::Var(Number {
8294 value: 2.0,
8295 units: NumericSuffix::Mm,
8296 }),
8297 },
8298 direction: None,
8299 construction: None,
8300 };
8301 let segments = vec![ExistingSegmentCtor {
8302 id: arc,
8303 ctor: SegmentCtor::Arc(arc_ctor),
8304 }];
8305 let (src_delta, scene_delta) = frontend
8306 .edit_segments(&mock_ctx, version, sketch_id, segments)
8307 .await
8308 .unwrap();
8309 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8310 assert_eq!(scene_delta.new_objects, vec![]);
8311 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8312
8313 ctx.close().await;
8314 mock_ctx.close().await;
8315 }
8316
8317 #[tokio::test(flavor = "multi_thread")]
8318 async fn test_new_sketch_add_circle_edit_circle() {
8319 let program = Program::empty();
8320
8321 let mut frontend = FrontendState::new();
8322 frontend.program = program;
8323
8324 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8325 let mock_ctx = ExecutorContext::new_mock(None).await;
8326 let version = Version(0);
8327
8328 let sketch_args = SketchCtor {
8329 on: Plane::Default(PlaneName::Xy),
8330 };
8331 let (_src_delta, _scene_delta, sketch_id) = frontend
8332 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8333 .await
8334 .unwrap();
8335
8336 let circle_ctor = CircleCtor {
8338 start: Point2d {
8339 x: Expr::Var(Number {
8340 value: 5.0,
8341 units: NumericSuffix::Mm,
8342 }),
8343 y: Expr::Var(Number {
8344 value: 0.0,
8345 units: NumericSuffix::Mm,
8346 }),
8347 },
8348 center: Point2d {
8349 x: Expr::Var(Number {
8350 value: 0.0,
8351 units: NumericSuffix::Mm,
8352 }),
8353 y: Expr::Var(Number {
8354 value: 0.0,
8355 units: NumericSuffix::Mm,
8356 }),
8357 },
8358 construction: None,
8359 };
8360 let segment = SegmentCtor::Circle(circle_ctor);
8361 let (src_delta, scene_delta) = frontend
8362 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8363 .await
8364 .unwrap();
8365 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8366 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8368 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8369
8370 let circle = *scene_delta.new_objects.last().unwrap();
8371
8372 let circle_ctor = CircleCtor {
8374 start: Point2d {
8375 x: Expr::Var(Number {
8376 value: 10.0,
8377 units: NumericSuffix::Mm,
8378 }),
8379 y: Expr::Var(Number {
8380 value: 0.0,
8381 units: NumericSuffix::Mm,
8382 }),
8383 },
8384 center: Point2d {
8385 x: Expr::Var(Number {
8386 value: 3.0,
8387 units: NumericSuffix::Mm,
8388 }),
8389 y: Expr::Var(Number {
8390 value: 4.0,
8391 units: NumericSuffix::Mm,
8392 }),
8393 },
8394 construction: None,
8395 };
8396 let segments = vec![ExistingSegmentCtor {
8397 id: circle,
8398 ctor: SegmentCtor::Circle(circle_ctor),
8399 }];
8400 let (src_delta, scene_delta) = frontend
8401 .edit_segments(&mock_ctx, version, sketch_id, segments)
8402 .await
8403 .unwrap();
8404 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8405 assert_eq!(scene_delta.new_objects, vec![]);
8406 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8407
8408 ctx.close().await;
8409 mock_ctx.close().await;
8410 }
8411
8412 #[tokio::test(flavor = "multi_thread")]
8413 async fn test_delete_circle() {
8414 let initial_source = "sketch001 = sketch(on = XY) {
8415 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8416}
8417";
8418
8419 let program = Program::parse(initial_source).unwrap().0.unwrap();
8420 let mut frontend = FrontendState::new();
8421
8422 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8423 let mock_ctx = ExecutorContext::new_mock(None).await;
8424 let version = Version(0);
8425
8426 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8427 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8428 let sketch_id = sketch_object.id;
8429 let sketch = expect_sketch(sketch_object);
8430
8431 assert_eq!(sketch.segments.len(), 3);
8433 let circle_id = sketch.segments[2];
8434
8435 let (src_delta, scene_delta) = frontend
8437 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8438 .await
8439 .unwrap();
8440 insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8441 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8442 let new_sketch = expect_sketch(new_sketch_object);
8443 assert_eq!(new_sketch.segments.len(), 0);
8444
8445 ctx.close().await;
8446 mock_ctx.close().await;
8447 }
8448
8449 #[tokio::test(flavor = "multi_thread")]
8450 async fn test_edit_circle_via_point() {
8451 let initial_source = "sketch001 = sketch(on = XY) {
8452 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8453}
8454";
8455
8456 let program = Program::parse(initial_source).unwrap().0.unwrap();
8457 let mut frontend = FrontendState::new();
8458
8459 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8460 let mock_ctx = ExecutorContext::new_mock(None).await;
8461 let version = Version(0);
8462
8463 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8464 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8465 let sketch_id = sketch_object.id;
8466 let sketch = expect_sketch(sketch_object);
8467
8468 let circle_id = sketch
8470 .segments
8471 .iter()
8472 .copied()
8473 .find(|seg_id| {
8474 matches!(
8475 &frontend.scene_graph.objects[seg_id.0].kind,
8476 ObjectKind::Segment {
8477 segment: Segment::Circle(_)
8478 }
8479 )
8480 })
8481 .expect("Expected a circle segment in sketch");
8482 let circle_object = &frontend.scene_graph.objects[circle_id.0];
8483 let ObjectKind::Segment {
8484 segment: Segment::Circle(circle),
8485 } = &circle_object.kind
8486 else {
8487 panic!("Expected circle segment, got: {:?}", circle_object.kind);
8488 };
8489 let start_point_id = circle.start;
8490
8491 let segments = vec![ExistingSegmentCtor {
8493 id: start_point_id,
8494 ctor: SegmentCtor::Point(PointCtor {
8495 position: Point2d {
8496 x: Expr::Var(Number {
8497 value: 7.0,
8498 units: NumericSuffix::Mm,
8499 }),
8500 y: Expr::Var(Number {
8501 value: 1.0,
8502 units: NumericSuffix::Mm,
8503 }),
8504 },
8505 }),
8506 }];
8507 let (src_delta, _scene_delta) = frontend
8508 .edit_segments(&mock_ctx, version, sketch_id, segments)
8509 .await
8510 .unwrap();
8511 insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8512
8513 ctx.close().await;
8514 mock_ctx.close().await;
8515 }
8516
8517 #[tokio::test(flavor = "multi_thread")]
8518 async fn test_add_line_when_sketch_block_uses_variable() {
8519 let initial_source = "s = sketch(on = XY) {}
8520";
8521
8522 let program = Program::parse(initial_source).unwrap().0.unwrap();
8523
8524 let mut frontend = FrontendState::new();
8525
8526 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8527 let mock_ctx = ExecutorContext::new_mock(None).await;
8528 let version = Version(0);
8529
8530 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8531 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8532 let sketch_id = sketch_object.id;
8533
8534 let line_ctor = LineCtor {
8535 start: Point2d {
8536 x: Expr::Number(Number {
8537 value: 0.0,
8538 units: NumericSuffix::Mm,
8539 }),
8540 y: Expr::Number(Number {
8541 value: 0.0,
8542 units: NumericSuffix::Mm,
8543 }),
8544 },
8545 end: Point2d {
8546 x: Expr::Number(Number {
8547 value: 10.0,
8548 units: NumericSuffix::Mm,
8549 }),
8550 y: Expr::Number(Number {
8551 value: 10.0,
8552 units: NumericSuffix::Mm,
8553 }),
8554 },
8555 construction: None,
8556 };
8557 let segment = SegmentCtor::Line(line_ctor);
8558 let (src_delta, scene_delta) = frontend
8559 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8560 .await
8561 .unwrap();
8562 insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8563 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8564 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8565
8566 ctx.close().await;
8567 mock_ctx.close().await;
8568 }
8569
8570 #[tokio::test(flavor = "multi_thread")]
8571 async fn test_new_sketch_add_line_delete_sketch() {
8572 let program = Program::empty();
8573
8574 let mut frontend = FrontendState::new();
8575 frontend.program = program;
8576
8577 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8578 let mock_ctx = ExecutorContext::new_mock(None).await;
8579 let version = Version(0);
8580
8581 let sketch_args = SketchCtor {
8582 on: Plane::Default(PlaneName::Xy),
8583 };
8584 let (_src_delta, scene_delta, sketch_id) = frontend
8585 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8586 .await
8587 .unwrap();
8588 assert_eq!(sketch_id, ObjectId(1));
8589 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8590 let sketch_object = &scene_delta.new_graph.objects[1];
8591 assert_eq!(sketch_object.id, ObjectId(1));
8592 assert_eq!(
8593 sketch_object.kind,
8594 ObjectKind::Sketch(Sketch {
8595 args: SketchCtor {
8596 on: Plane::Default(PlaneName::Xy)
8597 },
8598 plane: ObjectId(0),
8599 segments: vec![],
8600 constraints: vec![],
8601 })
8602 );
8603 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8604
8605 let line_ctor = LineCtor {
8606 start: Point2d {
8607 x: Expr::Number(Number {
8608 value: 0.0,
8609 units: NumericSuffix::Mm,
8610 }),
8611 y: Expr::Number(Number {
8612 value: 0.0,
8613 units: NumericSuffix::Mm,
8614 }),
8615 },
8616 end: Point2d {
8617 x: Expr::Number(Number {
8618 value: 10.0,
8619 units: NumericSuffix::Mm,
8620 }),
8621 y: Expr::Number(Number {
8622 value: 10.0,
8623 units: NumericSuffix::Mm,
8624 }),
8625 },
8626 construction: None,
8627 };
8628 let segment = SegmentCtor::Line(line_ctor);
8629 let (src_delta, scene_delta) = frontend
8630 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8631 .await
8632 .unwrap();
8633 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8634 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8635
8636 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8637 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8638 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8639
8640 ctx.close().await;
8641 mock_ctx.close().await;
8642 }
8643
8644 #[tokio::test(flavor = "multi_thread")]
8645 async fn test_delete_sketch_when_sketch_block_uses_variable() {
8646 let initial_source = "s = sketch(on = XY) {}
8647";
8648
8649 let program = Program::parse(initial_source).unwrap().0.unwrap();
8650
8651 let mut frontend = FrontendState::new();
8652
8653 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8654 let version = Version(0);
8655
8656 frontend.hack_set_program(&ctx, program).await.unwrap();
8657 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8658 let sketch_id = sketch_object.id;
8659
8660 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8661 insta::assert_snapshot!(
8662 "test_delete_sketch_when_sketch_block_uses_variable",
8663 src_delta.text.as_str()
8664 );
8665 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8666
8667 ctx.close().await;
8668 }
8669
8670 #[tokio::test(flavor = "multi_thread")]
8671 async fn test_delete_sketch_after_comment() {
8672 let initial_source = "sketch001 = sketch(on = XZ) {
8673}
8674";
8675
8676 let program = Program::parse(initial_source).unwrap().0.unwrap();
8677 let mut frontend = FrontendState::new();
8678
8679 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8680 let version = Version(0);
8681
8682 frontend.hack_set_program(&ctx, program).await.unwrap();
8683 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8684 let sketch_id = sketch_object.id;
8685 let original_source = sketch_object.source.clone();
8686
8687 let commented_source = "// test 1
8688sketch001 = sketch(on = XZ) {
8689}
8690";
8691 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8692 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8693
8694 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8695 assert_eq!(cached_sketch_object.source, original_source);
8696
8697 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8698 assert!(
8699 !src_delta.text.contains("sketch001"),
8700 "sketch was not deleted: {}",
8701 src_delta.text
8702 );
8703 insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8705 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8706
8707 ctx.close().await;
8708 }
8709
8710 #[tokio::test(flavor = "multi_thread")]
8711 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8712 let initial_source = "sketch001 = sketch(on = XZ) {
8713}
8714foo = 1
8715";
8716
8717 let program = Program::parse(initial_source).unwrap().0.unwrap();
8718 let mut frontend = FrontendState::new();
8719
8720 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8721 let version = Version(0);
8722
8723 frontend.hack_set_program(&ctx, program).await.unwrap();
8724 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8725 let sketch_id = sketch_object.id;
8726
8727 let commented_source = "// keep me
8728sketch001 = sketch(on = XZ) {
8729}
8730foo = 1
8731";
8732 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8733 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8734
8735 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8736 insta::assert_snapshot!(
8738 "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8739 src_delta.text.as_str()
8740 );
8741
8742 ctx.close().await;
8743 }
8744
8745 #[tokio::test(flavor = "multi_thread")]
8746 async fn test_delete_segment_preserves_pre_comment() {
8747 let initial_source = "\
8748sketch(on = XY) {
8749 point(at = [var 1, var 2])
8750 // describe the middle point
8751 point(at = [var 3, var 4])
8752 point(at = [var 5, var 6])
8753}
8754";
8755
8756 let program = Program::parse(initial_source).unwrap().0.unwrap();
8757 let mut frontend = FrontendState::new();
8758
8759 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8760 let mock_ctx = ExecutorContext::new_mock(None).await;
8761 let version = Version(0);
8762
8763 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8764 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8765 let sketch_id = sketch_object.id;
8766 let sketch = expect_sketch(sketch_object);
8767
8768 let middle_point_id = *sketch.segments.get(1).unwrap();
8769
8770 let (src_delta, _scene_delta) = frontend
8771 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8772 .await
8773 .unwrap();
8774 insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8777
8778 ctx.close().await;
8779 mock_ctx.close().await;
8780 }
8781
8782 #[tokio::test(flavor = "multi_thread")]
8783 async fn test_delete_last_segment_preserves_pre_comment() {
8784 let initial_source = "\
8785sketch(on = XY) {
8786 point(at = [var 1, var 2])
8787 // describe the trailing point
8788 point(at = [var 3, var 4])
8789}
8790";
8791
8792 let program = Program::parse(initial_source).unwrap().0.unwrap();
8793 let mut frontend = FrontendState::new();
8794
8795 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8796 let mock_ctx = ExecutorContext::new_mock(None).await;
8797 let version = Version(0);
8798
8799 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8800 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8801 let sketch_id = sketch_object.id;
8802 let sketch = expect_sketch(sketch_object);
8803
8804 let last_point_id = *sketch.segments.last().unwrap();
8805
8806 let (src_delta, _scene_delta) = frontend
8807 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8808 .await
8809 .unwrap();
8810 insta::assert_snapshot!(
8813 "test_delete_last_segment_preserves_pre_comment",
8814 src_delta.text.as_str()
8815 );
8816
8817 ctx.close().await;
8818 mock_ctx.close().await;
8819 }
8820
8821 #[tokio::test(flavor = "multi_thread")]
8822 async fn test_delete_segment_drops_inline_trailing_comment() {
8823 let initial_source = "\
8824sketch(on = XY) {
8825 point(at = [var 1, var 2])
8826 point(at = [var 3, var 4]) // same-line note that gets dropped
8827 point(at = [var 5, var 6])
8828}
8829";
8830
8831 let program = Program::parse(initial_source).unwrap().0.unwrap();
8832 let mut frontend = FrontendState::new();
8833
8834 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8835 let mock_ctx = ExecutorContext::new_mock(None).await;
8836 let version = Version(0);
8837
8838 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8839 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8840 let sketch_id = sketch_object.id;
8841 let sketch = expect_sketch(sketch_object);
8842
8843 let middle_point_id = *sketch.segments.get(1).unwrap();
8844
8845 let (src_delta, _scene_delta) = frontend
8846 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8847 .await
8848 .unwrap();
8849 assert!(
8851 !src_delta.text.contains("same-line note"),
8852 "inline comment should have been removed: {}",
8853 src_delta.text
8854 );
8855
8856 ctx.close().await;
8857 mock_ctx.close().await;
8858 }
8859
8860 #[tokio::test(flavor = "multi_thread")]
8861 async fn test_delete_segments_preserves_block_comments_across_positions() {
8862 let initial_source = "\
8870sketch(on = XY) {
8871 /* above first - moves to middle */
8872 point(at = [var 1, var 2]) /* same-line on first - dropped */
8873 /* above middle - stays */
8874 point(at = [var 3, var 4])
8875 /* above last - moves to trailing meta */
8876 point(at = [var 5, var 6])
8877}
8878";
8879
8880 let program = Program::parse(initial_source).unwrap().0.unwrap();
8881 let mut frontend = FrontendState::new();
8882
8883 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8884 let mock_ctx = ExecutorContext::new_mock(None).await;
8885 let version = Version(0);
8886
8887 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8888 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8889 let sketch_id = sketch_object.id;
8890 let sketch = expect_sketch(sketch_object);
8891
8892 let first_point_id = *sketch.segments.first().unwrap();
8893 let last_point_id = *sketch.segments.last().unwrap();
8894
8895 let (src_delta, _scene_delta) = frontend
8896 .delete_objects(
8897 &mock_ctx,
8898 version,
8899 sketch_id,
8900 Vec::new(),
8901 vec![first_point_id, last_point_id],
8902 )
8903 .await
8904 .unwrap();
8905 insta::assert_snapshot!(
8906 "test_delete_segments_preserves_block_comments_across_positions",
8907 src_delta.text.as_str()
8908 );
8909
8910 ctx.close().await;
8911 mock_ctx.close().await;
8912 }
8913
8914 #[tokio::test(flavor = "multi_thread")]
8915 async fn test_edit_line_when_editing_its_start_point() {
8916 let initial_source = "\
8917sketch(on = XY) {
8918 line(start = [var 1, var 2], end = [var 3, var 4])
8919}
8920";
8921
8922 let program = Program::parse(initial_source).unwrap().0.unwrap();
8923
8924 let mut frontend = FrontendState::new();
8925
8926 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8927 let mock_ctx = ExecutorContext::new_mock(None).await;
8928 let version = Version(0);
8929
8930 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8931 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8932 let sketch_id = sketch_object.id;
8933 let sketch = expect_sketch(sketch_object);
8934
8935 let point_id = *sketch.segments.first().unwrap();
8936
8937 let point_ctor = PointCtor {
8938 position: Point2d {
8939 x: Expr::Var(Number {
8940 value: 5.0,
8941 units: NumericSuffix::Inch,
8942 }),
8943 y: Expr::Var(Number {
8944 value: 6.0,
8945 units: NumericSuffix::Inch,
8946 }),
8947 },
8948 };
8949 let segments = vec![ExistingSegmentCtor {
8950 id: point_id,
8951 ctor: SegmentCtor::Point(point_ctor),
8952 }];
8953 let (src_delta, scene_delta) = frontend
8954 .edit_segments(&mock_ctx, version, sketch_id, segments)
8955 .await
8956 .unwrap();
8957 insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
8958 assert_eq!(scene_delta.new_objects, vec![]);
8959 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8960
8961 ctx.close().await;
8962 mock_ctx.close().await;
8963 }
8964
8965 #[tokio::test(flavor = "multi_thread")]
8966 async fn test_edit_line_when_editing_its_end_point() {
8967 let initial_source = "\
8968sketch(on = XY) {
8969 line(start = [var 1, var 2], end = [var 3, var 4])
8970}
8971";
8972
8973 let program = Program::parse(initial_source).unwrap().0.unwrap();
8974
8975 let mut frontend = FrontendState::new();
8976
8977 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8978 let mock_ctx = ExecutorContext::new_mock(None).await;
8979 let version = Version(0);
8980
8981 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8982 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8983 let sketch_id = sketch_object.id;
8984 let sketch = expect_sketch(sketch_object);
8985 let point_id = *sketch.segments.get(1).unwrap();
8986
8987 let point_ctor = PointCtor {
8988 position: Point2d {
8989 x: Expr::Var(Number {
8990 value: 5.0,
8991 units: NumericSuffix::Inch,
8992 }),
8993 y: Expr::Var(Number {
8994 value: 6.0,
8995 units: NumericSuffix::Inch,
8996 }),
8997 },
8998 };
8999 let segments = vec![ExistingSegmentCtor {
9000 id: point_id,
9001 ctor: SegmentCtor::Point(point_ctor),
9002 }];
9003 let (src_delta, scene_delta) = frontend
9004 .edit_segments(&mock_ctx, version, sketch_id, segments)
9005 .await
9006 .unwrap();
9007 insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9008 assert_eq!(scene_delta.new_objects, vec![]);
9009 assert_eq!(
9010 scene_delta.new_graph.objects.len(),
9011 5,
9012 "{:#?}",
9013 scene_delta.new_graph.objects
9014 );
9015
9016 ctx.close().await;
9017 mock_ctx.close().await;
9018 }
9019
9020 #[tokio::test(flavor = "multi_thread")]
9021 async fn test_edit_line_with_coincident_feedback() {
9022 let initial_source = "\
9023sketch(on = XY) {
9024 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9025 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9026 fixed([line1.start, [0, 0]])
9027 coincident([line1.end, line2.start])
9028 equalLength([line1, line2])
9029}
9030";
9031
9032 let program = Program::parse(initial_source).unwrap().0.unwrap();
9033
9034 let mut frontend = FrontendState::new();
9035
9036 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9037 let mock_ctx = ExecutorContext::new_mock(None).await;
9038 let version = Version(0);
9039
9040 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9041 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9042 let sketch_id = sketch_object.id;
9043 let sketch = expect_sketch(sketch_object);
9044 let line2_end_id = *sketch.segments.get(4).unwrap();
9045
9046 let segments = vec![ExistingSegmentCtor {
9047 id: line2_end_id,
9048 ctor: SegmentCtor::Point(PointCtor {
9049 position: Point2d {
9050 x: Expr::Var(Number {
9051 value: 9.0,
9052 units: NumericSuffix::None,
9053 }),
9054 y: Expr::Var(Number {
9055 value: 10.0,
9056 units: NumericSuffix::None,
9057 }),
9058 },
9059 }),
9060 }];
9061 let (src_delta, scene_delta) = frontend
9062 .edit_segments(&mock_ctx, version, sketch_id, segments)
9063 .await
9064 .unwrap();
9065 insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9066 assert_eq!(
9067 scene_delta.new_graph.objects.len(),
9068 11,
9069 "{:#?}",
9070 scene_delta.new_graph.objects
9071 );
9072
9073 ctx.close().await;
9074 mock_ctx.close().await;
9075 }
9076
9077 #[tokio::test(flavor = "multi_thread")]
9078 async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9079 let initial_source = "\
9080sketch(on = XY) {
9081 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9082 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9083 fixed([line1.start, [0, 0]])
9084 coincident([line1.end, line2.start])
9085 equalLength([line1, line2])
9086}
9087";
9088
9089 let program = Program::parse(initial_source).unwrap().0.unwrap();
9090 let mut frontend = FrontendState::new();
9091 let mock_ctx = ExecutorContext::new_mock(None).await;
9092 let version = Version(0);
9093
9094 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9095 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9096 let sketch_id = sketch_object.id;
9097 let sketch = expect_sketch(sketch_object);
9098 let line2_end_id = *sketch.segments.get(4).unwrap();
9099
9100 let segments = vec![ExistingSegmentCtor {
9101 id: line2_end_id,
9102 ctor: SegmentCtor::Point(PointCtor {
9103 position: Point2d {
9104 x: Expr::Var(Number {
9105 value: 9.0,
9106 units: NumericSuffix::None,
9107 }),
9108 y: Expr::Var(Number {
9109 value: 10.0,
9110 units: NumericSuffix::None,
9111 }),
9112 },
9113 }),
9114 }];
9115 let (edited_source, _) = frontend
9116 .edit_segments(&mock_ctx, version, sketch_id, segments)
9117 .await
9118 .unwrap();
9119
9120 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9121 assert_eq!(mock_source.text, edited_source.text);
9122
9123 mock_ctx.close().await;
9124 }
9125
9126 #[tokio::test(flavor = "multi_thread")]
9129 async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9130 let initial_source = "\
9131sketch(on = XY) {
9132 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9133 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9134 fixed([line1.start, [0, 0]])
9135 coincident([line1.end, line2.start])
9136 equalLength([line1, line2])
9137}
9138";
9139
9140 let program = Program::parse(initial_source).unwrap().0.unwrap();
9141 let mut frontend = FrontendState::new();
9142 let mock_ctx = ExecutorContext::new_mock(None).await;
9143 let version = Version(0);
9144
9145 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9146 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9147 let sketch_id = sketch_object.id;
9148 let sketch = expect_sketch(sketch_object);
9149 let line2_end_id = *sketch.segments.get(4).unwrap();
9150
9151 let segments = vec![ExistingSegmentCtor {
9152 id: line2_end_id,
9153 ctor: SegmentCtor::Point(PointCtor {
9154 position: Point2d {
9155 x: Expr::Var(Number {
9156 value: 9.0,
9157 units: NumericSuffix::None,
9158 }),
9159 y: Expr::Var(Number {
9160 value: 10.0,
9161 units: NumericSuffix::None,
9162 }),
9163 },
9164 }),
9165 }];
9166 let (preview_source, preview_delta) = frontend
9167 .edit_segments_with_options(
9168 &mock_ctx,
9169 version,
9170 sketch_id,
9171 segments,
9172 EditSegmentsOptions {
9173 anchor_segment_ids: Some(vec![line2_end_id]),
9174 drag_anchors: Vec::new(),
9175 constraint_label_edits: Vec::new(),
9176 commit_solved_initial_guesses: false,
9177 },
9178 )
9179 .await
9180 .unwrap();
9181
9182 assert!(
9183 !preview_delta.exec_outcome.var_solutions.is_empty(),
9184 "preview solve should still solve and return geometry feedback"
9185 );
9186 assert!(
9187 preview_source
9188 .text
9189 .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9190 );
9191 assert!(
9192 preview_source
9193 .text
9194 .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9195 );
9196
9197 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9198 assert_eq!(mock_source.text, preview_source.text);
9199
9200 mock_ctx.close().await;
9201 }
9202
9203 #[tokio::test(flavor = "multi_thread")]
9204 async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9205 let initial_source = "\
9206sketch(on = XY) {
9207 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9208}
9209";
9210
9211 let program = Program::parse(initial_source).unwrap().0.unwrap();
9212 let mut frontend = FrontendState::new();
9213 let mock_ctx = ExecutorContext::new_mock(None).await;
9214 let version = Version(0);
9215
9216 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9217 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9218 let sketch_id = sketch_object.id;
9219 let sketch = expect_sketch(sketch_object);
9220 let line_end_id = *sketch.segments.get(1).unwrap();
9221
9222 let constraint = Constraint::Fixed(Fixed {
9223 points: vec![FixedPoint {
9224 point: line_end_id,
9225 position: Point2d {
9226 x: Number {
9227 value: 20.0,
9228 units: NumericSuffix::Mm,
9229 },
9230 y: Number {
9231 value: 0.0,
9232 units: NumericSuffix::Mm,
9233 },
9234 },
9235 }],
9236 });
9237 let (constraint_source, _) = frontend
9238 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9239 .await
9240 .unwrap();
9241
9242 assert!(
9243 constraint_source
9244 .text
9245 .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9246 "{}",
9247 constraint_source.text
9248 );
9249 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9250 assert_eq!(mock_source.text, constraint_source.text);
9251
9252 mock_ctx.close().await;
9253 }
9254
9255 #[test]
9256 fn test_no_solver_feedback_preserves_original_source() {
9257 let initial_source = "\
9258@settings(defaultLengthUnit = in, kclVersion = 2.0)
9259cylinder = startSketchOn(XY)
9260 |> circle(center= [0, 0], radius= 22)
9261 |> extrude(length = 14)
9262";
9263 let mut frontend = FrontendState::new();
9264 frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9265 let outcome = ExecOutcome {
9266 variables: Default::default(),
9267 test_program_memory: Default::default(),
9268 operations: Default::default(),
9269 artifact_graph: Default::default(),
9270 scene_objects: Default::default(),
9271 source_range_to_object: Default::default(),
9272 var_solutions: Default::default(),
9273 refactor_metadata: Default::default(),
9274 issues: Default::default(),
9275 filenames: Default::default(),
9276 source_files: Default::default(),
9277 default_planes: Default::default(),
9278 };
9279
9280 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9281
9282 assert_eq!(source_delta.text, initial_source);
9283 }
9284
9285 #[tokio::test(flavor = "multi_thread")]
9288 async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9289 let initial_source = "\
9290sketch(on = XY) {
9291 point1 = point(at = [var 0mm, var 0mm])
9292 point2 = point(at = [var 0mm, var 0mm])
9293 coincident([point1, point2])
9294}
9295";
9296
9297 let program = Program::parse(initial_source).unwrap().0.unwrap();
9298 let mut frontend = FrontendState::new();
9299 let mock_ctx = ExecutorContext::new_mock(None).await;
9300 let version = Version(0);
9301
9302 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9303 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9304 let sketch_id = sketch_object.id;
9305 let sketch = expect_sketch(sketch_object);
9306 let point1_id = sketch.segments[0];
9307 let point2_id = sketch.segments[1];
9308
9309 let segments = vec![
9310 ExistingSegmentCtor {
9311 id: point1_id,
9312 ctor: SegmentCtor::Point(PointCtor {
9313 position: point_expr_mm(10.0, 0.0),
9314 }),
9315 },
9316 ExistingSegmentCtor {
9317 id: point2_id,
9318 ctor: SegmentCtor::Point(PointCtor {
9319 position: point_expr_mm(100.0, 0.0),
9320 }),
9321 },
9322 ];
9323 let (_, scene_delta) = frontend
9324 .edit_segments_with_options(
9325 &mock_ctx,
9326 version,
9327 sketch_id,
9328 segments,
9329 EditSegmentsOptions {
9330 anchor_segment_ids: Some(vec![point1_id]),
9331 drag_anchors: Vec::new(),
9332 constraint_label_edits: Vec::new(),
9333 commit_solved_initial_guesses: true,
9334 },
9335 )
9336 .await
9337 .unwrap();
9338
9339 assert_point_position_close(
9340 point_position(&scene_delta.new_graph, point1_id),
9341 point_number_mm(10.0, 0.0),
9342 );
9343 assert_point_position_close(
9344 point_position(&scene_delta.new_graph, point2_id),
9345 point_number_mm(10.0, 0.0),
9346 );
9347
9348 mock_ctx.close().await;
9349 }
9350
9351 fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9354 use std::cell::RefCell;
9355 struct Collector {
9356 target: f64,
9357 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9358 }
9359 impl<'a> crate::walk::Visitor<'a> for &Collector {
9360 type Error = crate::front::Error;
9361 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9362 if let crate::walk::Node::SketchVar(sketch_var) = node
9363 && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9364 && (initial.value - self.target).abs() < 1e-9
9365 {
9366 self.out
9367 .borrow_mut()
9368 .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9369 }
9370 for child in node.children().iter() {
9371 if !child.visit(*self)? {
9372 return Ok(false);
9373 }
9374 }
9375 Ok(true)
9376 }
9377 }
9378 let collector = Collector {
9379 target: value,
9380 out: Default::default(),
9381 };
9382 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9383 collector.out.into_inner()
9384 }
9385
9386 fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9389 use std::cell::RefCell;
9390 struct Collector {
9391 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9392 }
9393 impl<'a> crate::walk::Visitor<'a> for &Collector {
9394 type Error = crate::front::Error;
9395 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9396 if let crate::walk::Node::SketchVar(sketch_var) = node
9397 && let Some(node_path) = &sketch_var.node_path
9398 {
9399 self.out
9400 .borrow_mut()
9401 .push((SourceRange::from(sketch_var), node_path.clone()));
9402 }
9403 for child in node.children().iter() {
9404 if !child.visit(*self)? {
9405 return Ok(false);
9406 }
9407 }
9408 Ok(true)
9409 }
9410 }
9411 let collector = Collector {
9412 out: Default::default(),
9413 };
9414 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9415 collector.out.into_inner()
9416 }
9417
9418 fn empty_exec_outcome_with_var_solutions(
9419 var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9420 ) -> ExecOutcome {
9421 ExecOutcome {
9422 variables: Default::default(),
9423 test_program_memory: Default::default(),
9424 operations: Default::default(),
9425 artifact_graph: Default::default(),
9426 scene_objects: Default::default(),
9427 source_range_to_object: Default::default(),
9428 var_solutions,
9429 refactor_metadata: Default::default(),
9430 issues: Default::default(),
9431 filenames: Default::default(),
9432 source_files: Default::default(),
9433 default_planes: Default::default(),
9434 }
9435 }
9436
9437 #[test]
9441 fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9442 let initial_source = "\
9443sketch(on = XY) {
9444 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9445}
9446";
9447 let program = Program::parse(initial_source).unwrap().0.unwrap();
9448 let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9449 assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9450 let (literal_range, node_path) = matches.into_iter().next().unwrap();
9451
9452 let mut frontend = FrontendState::new();
9453 frontend.program = program;
9454
9455 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9456 literal_range,
9457 Some(node_path),
9458 Number {
9459 value: 25.0,
9460 units: NumericSuffix::Mm,
9461 },
9462 )]);
9463
9464 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9465
9466 insta::assert_snapshot!(
9467 "test_commit_var_solution_by_node_path_updates_sketch_var",
9468 source_delta.text
9469 );
9470 }
9471
9472 #[test]
9479 fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9480 let compact_source = "\
9481sketch(on = XY) {
9482 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9483}
9484";
9485 let padded_source = "\
9486// added comment\n// added comment\n\nsketch(on = XY) {
9487 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9488}
9489";
9490 let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9491 let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9492
9493 let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9494 .into_iter()
9495 .next()
9496 .expect("expected `var 10mm` in compact source");
9497 let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9498 .into_iter()
9499 .next()
9500 .expect("expected `var 10mm` in padded source");
9501
9502 assert_ne!(
9503 compact_match.0, padded_match.0,
9504 "byte offsets must differ for this test to be meaningful"
9505 );
9506 assert_eq!(
9507 compact_match.1, padded_match.1,
9508 "node paths must agree across whitespace; that's the whole point of NodePath",
9509 );
9510
9511 let mut frontend = FrontendState::new();
9512 frontend.program = padded_program;
9513
9514 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9516 compact_match.0,
9517 Some(compact_match.1),
9518 Number {
9519 value: 30.0,
9520 units: NumericSuffix::Mm,
9521 },
9522 )]);
9523
9524 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9525
9526 insta::assert_snapshot!(
9527 "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9528 source_delta.text
9529 );
9530 }
9531
9532 #[test]
9536 fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9537 let initial_source = "\
9538sketch(on = XY) {
9539 line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9540}
9541";
9542 let program = Program::parse(initial_source).unwrap().0.unwrap();
9543
9544 let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9545 .into_iter()
9546 .next()
9547 .expect("expected `var 10mm`");
9548 let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9549 .into_iter()
9550 .next()
9551 .expect("expected `var 20mm`");
9552
9553 let mut frontend = FrontendState::new();
9554 frontend.program = program;
9555
9556 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9558 var_20.0,
9559 Some(var_10.1),
9560 Number {
9561 value: 33.0,
9562 units: NumericSuffix::Mm,
9563 },
9564 )]);
9565
9566 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9567
9568 insta::assert_snapshot!(
9569 "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9570 source_delta.text
9571 );
9572 }
9573
9574 #[test]
9581 fn test_commit_var_solution_writes_back_into_bare_var() {
9582 let initial_source = "\
9583@settings(experimentalFeatures = allow, kclVersion = 2.0)
9584sketch(on = XY) {
9585 line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9586}
9587";
9588 let program = Program::parse(initial_source).unwrap().0.unwrap();
9589
9590 let bare = collect_all_sketch_vars(&program)
9593 .into_iter()
9594 .find(|(range, _)| {
9595 range.end() - range.start() == 3
9597 })
9598 .expect("expected at least one bare `var`");
9599
9600 let mut frontend = FrontendState::new();
9601 frontend.program = program;
9602
9603 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9604 bare.0,
9605 Some(bare.1),
9606 Number {
9607 value: 7.0,
9608 units: NumericSuffix::Mm,
9609 },
9610 )]);
9611
9612 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9613
9614 insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9618 }
9619
9620 #[tokio::test(flavor = "multi_thread")]
9621 async fn test_delete_point_without_var() {
9622 let initial_source = "\
9623sketch(on = XY) {
9624 point(at = [var 1, var 2])
9625 point(at = [var 3, var 4])
9626 point(at = [var 5, var 6])
9627}
9628";
9629
9630 let program = Program::parse(initial_source).unwrap().0.unwrap();
9631
9632 let mut frontend = FrontendState::new();
9633
9634 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9635 let mock_ctx = ExecutorContext::new_mock(None).await;
9636 let version = Version(0);
9637
9638 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9639 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9640 let sketch_id = sketch_object.id;
9641 let sketch = expect_sketch(sketch_object);
9642
9643 let point_id = *sketch.segments.get(1).unwrap();
9644
9645 let (src_delta, scene_delta) = frontend
9646 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9647 .await
9648 .unwrap();
9649 insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9650 assert_eq!(scene_delta.new_objects, vec![]);
9651 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9652
9653 ctx.close().await;
9654 mock_ctx.close().await;
9655 }
9656
9657 #[tokio::test(flavor = "multi_thread")]
9658 async fn test_delete_point_with_var() {
9659 let initial_source = "\
9660sketch(on = XY) {
9661 point(at = [var 1, var 2])
9662 point1 = point(at = [var 3, var 4])
9663 point(at = [var 5, var 6])
9664}
9665";
9666
9667 let program = Program::parse(initial_source).unwrap().0.unwrap();
9668
9669 let mut frontend = FrontendState::new();
9670
9671 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9672 let mock_ctx = ExecutorContext::new_mock(None).await;
9673 let version = Version(0);
9674
9675 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9676 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9677 let sketch_id = sketch_object.id;
9678 let sketch = expect_sketch(sketch_object);
9679
9680 let point_id = *sketch.segments.get(1).unwrap();
9681
9682 let (src_delta, scene_delta) = frontend
9683 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9684 .await
9685 .unwrap();
9686 insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9687 assert_eq!(scene_delta.new_objects, vec![]);
9688 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9689
9690 ctx.close().await;
9691 mock_ctx.close().await;
9692 }
9693
9694 #[tokio::test(flavor = "multi_thread")]
9695 async fn test_delete_multiple_points() {
9696 let initial_source = "\
9697sketch(on = XY) {
9698 point(at = [var 1, var 2])
9699 point1 = point(at = [var 3, var 4])
9700 point(at = [var 5, var 6])
9701}
9702";
9703
9704 let program = Program::parse(initial_source).unwrap().0.unwrap();
9705
9706 let mut frontend = FrontendState::new();
9707
9708 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9709 let mock_ctx = ExecutorContext::new_mock(None).await;
9710 let version = Version(0);
9711
9712 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9713 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9714 let sketch_id = sketch_object.id;
9715
9716 let sketch = expect_sketch(sketch_object);
9717
9718 let point1_id = *sketch.segments.first().unwrap();
9719 let point2_id = *sketch.segments.get(1).unwrap();
9720
9721 let (src_delta, scene_delta) = frontend
9722 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9723 .await
9724 .unwrap();
9725 insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9726 assert_eq!(scene_delta.new_objects, vec![]);
9727 assert_eq!(scene_delta.new_graph.objects.len(), 3);
9728
9729 ctx.close().await;
9730 mock_ctx.close().await;
9731 }
9732
9733 #[tokio::test(flavor = "multi_thread")]
9734 async fn test_delete_coincident_constraint() {
9735 let initial_source = "\
9736sketch(on = XY) {
9737 point1 = point(at = [var 1, var 2])
9738 point2 = point(at = [var 3, var 4])
9739 coincident([point1, point2])
9740 point(at = [var 5, var 6])
9741}
9742";
9743
9744 let program = Program::parse(initial_source).unwrap().0.unwrap();
9745
9746 let mut frontend = FrontendState::new();
9747
9748 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9749 let mock_ctx = ExecutorContext::new_mock(None).await;
9750 let version = Version(0);
9751
9752 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9753 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9754 let sketch_id = sketch_object.id;
9755 let sketch = expect_sketch(sketch_object);
9756
9757 let coincident_id = *sketch.constraints.first().unwrap();
9758
9759 let (src_delta, scene_delta) = frontend
9760 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9761 .await
9762 .unwrap();
9763 insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9764 assert_eq!(scene_delta.new_objects, vec![]);
9765 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9766
9767 ctx.close().await;
9768 mock_ctx.close().await;
9769 }
9770
9771 #[tokio::test(flavor = "multi_thread")]
9772 async fn test_delete_line_cascades_to_coincident_constraint() {
9773 let initial_source = "\
9774sketch(on = XY) {
9775 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9776 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9777 coincident([line1.end, line2.start])
9778}
9779";
9780
9781 let program = Program::parse(initial_source).unwrap().0.unwrap();
9782
9783 let mut frontend = FrontendState::new();
9784
9785 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9786 let mock_ctx = ExecutorContext::new_mock(None).await;
9787 let version = Version(0);
9788
9789 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9790 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9791 let sketch_id = sketch_object.id;
9792 let sketch = expect_sketch(sketch_object);
9793 let line_id = *sketch.segments.get(5).unwrap();
9794
9795 let (src_delta, scene_delta) = frontend
9796 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9797 .await
9798 .unwrap();
9799 insta::assert_snapshot!(
9800 "test_delete_line_cascades_to_coincident_constraint",
9801 src_delta.text.as_str()
9802 );
9803 assert_eq!(
9804 scene_delta.new_graph.objects.len(),
9805 5,
9806 "{:#?}",
9807 scene_delta.new_graph.objects
9808 );
9809
9810 ctx.close().await;
9811 mock_ctx.close().await;
9812 }
9813
9814 #[tokio::test(flavor = "multi_thread")]
9815 async fn test_delete_line_cascades_to_distance_constraint() {
9816 let initial_source = "\
9817sketch(on = XY) {
9818 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9819 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9820 distance([line1.end, line2.start]) == 10mm
9821}
9822";
9823
9824 let program = Program::parse(initial_source).unwrap().0.unwrap();
9825
9826 let mut frontend = FrontendState::new();
9827
9828 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9829 let mock_ctx = ExecutorContext::new_mock(None).await;
9830 let version = Version(0);
9831
9832 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9833 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9834 let sketch_id = sketch_object.id;
9835 let sketch = expect_sketch(sketch_object);
9836 let line_id = *sketch.segments.get(5).unwrap();
9837
9838 let (src_delta, scene_delta) = frontend
9839 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9840 .await
9841 .unwrap();
9842 insta::assert_snapshot!(
9843 "test_delete_line_cascades_to_distance_constraint",
9844 src_delta.text.as_str()
9845 );
9846 assert_eq!(
9847 scene_delta.new_graph.objects.len(),
9848 5,
9849 "{:#?}",
9850 scene_delta.new_graph.objects
9851 );
9852
9853 ctx.close().await;
9854 mock_ctx.close().await;
9855 }
9856
9857 #[tokio::test(flavor = "multi_thread")]
9858 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9859 let initial_source = "\
9860sketch(on = XY) {
9861 point1 = point(at = [var 1, var 2])
9862 point2 = point(at = [var 3, var 4])
9863 horizontalDistance([point1, point2]) == 10mm
9864}
9865";
9866
9867 let program = Program::parse(initial_source).unwrap().0.unwrap();
9868
9869 let mut frontend = FrontendState::new();
9870
9871 let mock_ctx = ExecutorContext::new_mock(None).await;
9872 let version = Version(0);
9873
9874 frontend.program = program.clone();
9875 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9876 frontend.update_state_after_exec(outcome, true);
9877 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9878 let sketch_id = sketch_object.id;
9879 let sketch = expect_sketch(sketch_object);
9880 let point2_id = *sketch.segments.get(1).unwrap();
9881
9882 let (src_delta, scene_delta) = frontend
9883 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9884 .await
9885 .unwrap();
9886 insta::assert_snapshot!(
9887 "test_delete_point_cascades_to_horizontal_distance_constraint",
9888 src_delta.text.as_str()
9889 );
9890 assert_eq!(
9891 scene_delta.new_graph.objects.len(),
9892 3,
9893 "{:#?}",
9894 scene_delta.new_graph.objects
9895 );
9896
9897 mock_ctx.close().await;
9898 }
9899
9900 #[tokio::test(flavor = "multi_thread")]
9901 async fn test_delete_line_cascades_to_fixed_constraint() {
9902 let initial_source = "\
9903sketch(on = XY) {
9904 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9905 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9906 fixed([line1.start, [0, 0]])
9907}
9908";
9909
9910 let program = Program::parse(initial_source).unwrap().0.unwrap();
9911
9912 let mut frontend = FrontendState::new();
9913
9914 let mock_ctx = ExecutorContext::new_mock(None).await;
9915 let version = Version(0);
9916
9917 frontend.program = program.clone();
9918 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9919 frontend.update_state_after_exec(outcome, true);
9920 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9921 let sketch_id = sketch_object.id;
9922 let sketch = expect_sketch(sketch_object);
9923 let line1_id = *sketch.segments.get(2).unwrap();
9924
9925 let (src_delta, scene_delta) = frontend
9926 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9927 .await
9928 .unwrap();
9929 insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9930 assert_eq!(
9931 scene_delta.new_graph.objects.len(),
9932 5,
9933 "{:#?}",
9934 scene_delta.new_graph.objects
9935 );
9936
9937 mock_ctx.close().await;
9938 }
9939
9940 #[tokio::test(flavor = "multi_thread")]
9941 async fn test_delete_line_cascades_to_midpoint_constraint() {
9942 let initial_source = "\
9943sketch(on = XY) {
9944 point1 = point(at = [var 1, var 2])
9945 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9946 midpoint(line1, point = point1)
9947}
9948";
9949
9950 let program = Program::parse(initial_source).unwrap().0.unwrap();
9951
9952 let mut frontend = FrontendState::new();
9953
9954 let mock_ctx = ExecutorContext::new_mock(None).await;
9955 let version = Version(0);
9956
9957 frontend.program = program.clone();
9958 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9959 frontend.update_state_after_exec(outcome, true);
9960 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9961 let sketch_id = sketch_object.id;
9962 let sketch = expect_sketch(sketch_object);
9963 let line1_id = *sketch.segments.get(3).unwrap();
9964
9965 let (src_delta, scene_delta) = frontend
9966 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9967 .await
9968 .unwrap();
9969 insta::assert_snapshot!(
9970 "test_delete_line_cascades_to_midpoint_constraint",
9971 src_delta.text.as_str()
9972 );
9973 assert_eq!(
9974 scene_delta.new_graph.objects.len(),
9975 3,
9976 "{:#?}",
9977 scene_delta.new_graph.objects
9978 );
9979
9980 mock_ctx.close().await;
9981 }
9982
9983 #[tokio::test(flavor = "multi_thread")]
9984 async fn test_delete_point_preserves_multiline_coincident_constraint() {
9985 let initial_source = "\
9986sketch(on = XY) {
9987 point1 = point(at = [var 1, var 2])
9988 point2 = point(at = [var 3, var 4])
9989 point3 = point(at = [var 5, var 6])
9990 coincident([point1, point2, point3])
9991}
9992";
9993
9994 let program = Program::parse(initial_source).unwrap().0.unwrap();
9995
9996 let mut frontend = FrontendState::new();
9997
9998 let mock_ctx = ExecutorContext::new_mock(None).await;
9999 let version = Version(0);
10000
10001 frontend.program = program.clone();
10002 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10003 frontend.update_state_after_exec(outcome, true);
10004 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10005 let sketch_id = sketch_object.id;
10006 let sketch = expect_sketch(sketch_object);
10007 let point3_id = *sketch.segments.get(2).unwrap();
10008
10009 let (src_delta, scene_delta) = frontend
10010 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10011 .await
10012 .unwrap();
10013 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10014 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10015 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10016 assert!(
10017 src_delta.text.contains("coincident([point1, point2])"),
10018 "{}",
10019 src_delta.text
10020 );
10021
10022 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10023 let sketch = expect_sketch(sketch_object);
10024 assert_eq!(sketch.segments.len(), 2);
10025 assert_eq!(sketch.constraints.len(), 1);
10026
10027 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10028 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10029 panic!("Expected constraint object");
10030 };
10031 let Constraint::Coincident(coincident) = constraint else {
10032 panic!("Expected coincident constraint");
10033 };
10034 assert_eq!(
10035 coincident.segments,
10036 sketch
10037 .segments
10038 .iter()
10039 .copied()
10040 .map(Into::into)
10041 .collect::<Vec<ConstraintSegment>>()
10042 );
10043
10044 mock_ctx.close().await;
10045 }
10046
10047 #[tokio::test(flavor = "multi_thread")]
10048 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10049 let initial_source = "\
10050sketch(on = XY) {
10051 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10052 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10053 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10054 equalLength([line1, line2, line3])
10055}
10056";
10057
10058 let program = Program::parse(initial_source).unwrap().0.unwrap();
10059
10060 let mut frontend = FrontendState::new();
10061
10062 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10063 let mock_ctx = ExecutorContext::new_mock(None).await;
10064 let version = Version(0);
10065
10066 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10067 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10068 let sketch_id = sketch_object.id;
10069 let sketch = expect_sketch(sketch_object);
10070 let line3_id = *sketch.segments.get(8).unwrap();
10071
10072 let (src_delta, scene_delta) = frontend
10073 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10074 .await
10075 .unwrap();
10076 insta::assert_snapshot!(
10077 "test_delete_line_preserves_multiline_equal_length_constraint",
10078 src_delta.text.as_str()
10079 );
10080
10081 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10082 let sketch = expect_sketch(sketch_object);
10083 assert_eq!(sketch.constraints.len(), 1);
10084
10085 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10086 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10087 panic!("Expected constraint object");
10088 };
10089 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10090 panic!("Expected lines equal length constraint");
10091 };
10092 assert_eq!(lines_equal_length.lines.len(), 2);
10093
10094 ctx.close().await;
10095 mock_ctx.close().await;
10096 }
10097
10098 #[tokio::test(flavor = "multi_thread")]
10099 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10100 let initial_source = "\
10101sketch(on = XY) {
10102 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10103 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10104 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10105 horizontal([line1.end, line2.start, line3.start])
10106}
10107";
10108
10109 let program = Program::parse(initial_source).unwrap().0.unwrap();
10110
10111 let mut frontend = FrontendState::new();
10112
10113 let mock_ctx = ExecutorContext::new_mock(None).await;
10114 let version = Version(0);
10115
10116 frontend.program = program.clone();
10117 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10118 frontend.update_state_after_exec(outcome, true);
10119 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10120 let sketch_id = sketch_object.id;
10121 let sketch = expect_sketch(sketch_object);
10122 let line1_id = *sketch.segments.get(2).unwrap();
10123
10124 let (src_delta, scene_delta) = frontend
10125 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10126 .await
10127 .unwrap();
10128 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10129 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10130 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10131 assert!(
10132 src_delta.text.contains("horizontal([line2.start, line3.start])"),
10133 "{}",
10134 src_delta.text
10135 );
10136
10137 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10138 let sketch = expect_sketch(sketch_object);
10139 assert_eq!(sketch.constraints.len(), 1);
10140
10141 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10142 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10143 panic!("Expected constraint object");
10144 };
10145 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10146 panic!("Expected horizontal points constraint");
10147 };
10148 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10149 assert_eq!(*points, remaining_points);
10150
10151 mock_ctx.close().await;
10152 }
10153
10154 #[tokio::test(flavor = "multi_thread")]
10155 async fn test_delete_line_preserves_multiline_vertical_constraint() {
10156 let initial_source = "\
10157sketch(on = XY) {
10158 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10159 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10160 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10161 vertical([line1.end, line2.start, line3.start])
10162}
10163";
10164
10165 let program = Program::parse(initial_source).unwrap().0.unwrap();
10166
10167 let mut frontend = FrontendState::new();
10168
10169 let mock_ctx = ExecutorContext::new_mock(None).await;
10170 let version = Version(0);
10171
10172 frontend.program = program.clone();
10173 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10174 frontend.update_state_after_exec(outcome, true);
10175 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10176 let sketch_id = sketch_object.id;
10177 let sketch = expect_sketch(sketch_object);
10178 let line1_id = *sketch.segments.get(2).unwrap();
10179
10180 let (src_delta, scene_delta) = frontend
10181 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10182 .await
10183 .unwrap();
10184 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10185 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10186 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10187 assert!(
10188 src_delta.text.contains("vertical([line2.start, line3.start])"),
10189 "{}",
10190 src_delta.text
10191 );
10192
10193 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10194 let sketch = expect_sketch(sketch_object);
10195 assert_eq!(sketch.constraints.len(), 1);
10196
10197 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10198 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10199 panic!("Expected constraint object");
10200 };
10201 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10202 panic!("Expected vertical points constraint");
10203 };
10204 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10205 assert_eq!(*points, remaining_points);
10206
10207 mock_ctx.close().await;
10208 }
10209
10210 #[tokio::test(flavor = "multi_thread")]
10211 async fn test_delete_line_preserves_multiline_coincident_constraint() {
10212 let initial_source = "\
10213sketch(on = XY) {
10214 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10215 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10216 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10217 coincident([line1.end, line2.start, line3.start])
10218}
10219";
10220
10221 let program = Program::parse(initial_source).unwrap().0.unwrap();
10222
10223 let mut frontend = FrontendState::new();
10224
10225 let mock_ctx = ExecutorContext::new_mock(None).await;
10226 let version = Version(0);
10227
10228 frontend.program = program.clone();
10229 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10230 frontend.update_state_after_exec(outcome, true);
10231 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10232 let sketch_id = sketch_object.id;
10233 let sketch = expect_sketch(sketch_object);
10234 let line1_id = *sketch.segments.get(2).unwrap();
10235
10236 let (src_delta, scene_delta) = frontend
10237 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10238 .await
10239 .unwrap();
10240 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10241 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10242 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10243 assert!(
10244 src_delta.text.contains("coincident([line2.start, line3.start])"),
10245 "{}",
10246 src_delta.text
10247 );
10248
10249 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10250 let sketch = expect_sketch(sketch_object);
10251 assert_eq!(sketch.constraints.len(), 1);
10252
10253 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10254 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10255 panic!("Expected constraint object");
10256 };
10257 let Constraint::Coincident(coincident) = constraint else {
10258 panic!("Expected coincident constraint");
10259 };
10260 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10261 assert_eq!(coincident.segments, remaining_segments);
10262
10263 mock_ctx.close().await;
10264 }
10265
10266 #[tokio::test(flavor = "multi_thread")]
10267 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10268 let initial_source = "\
10269sketch(on = XY) {
10270 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10271 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10272 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10273 equalLength([line1, line2, line3])
10274}
10275";
10276
10277 let program = Program::parse(initial_source).unwrap().0.unwrap();
10278
10279 let mut frontend = FrontendState::new();
10280
10281 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10282 let mock_ctx = ExecutorContext::new_mock(None).await;
10283 let version = Version(0);
10284
10285 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10286 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10287 let sketch_id = sketch_object.id;
10288 let sketch = expect_sketch(sketch_object);
10289 let line2_id = *sketch.segments.get(5).unwrap();
10290 let line3_id = *sketch.segments.get(8).unwrap();
10291
10292 let (src_delta, scene_delta) = frontend
10293 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10294 .await
10295 .unwrap();
10296 insta::assert_snapshot!(
10297 "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10298 src_delta.text.as_str()
10299 );
10300
10301 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10302 let sketch = expect_sketch(sketch_object);
10303 assert!(sketch.constraints.is_empty());
10304
10305 ctx.close().await;
10306 mock_ctx.close().await;
10307 }
10308
10309 #[tokio::test(flavor = "multi_thread")]
10310 async fn test_delete_line_preserves_multiline_parallel_constraint() {
10311 let initial_source = "\
10312sketch(on = XY) {
10313 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10314 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10315 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10316 parallel([line1, line2, line3])
10317}
10318";
10319
10320 let program = Program::parse(initial_source).unwrap().0.unwrap();
10321
10322 let mut frontend = FrontendState::new();
10323
10324 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10325 let mock_ctx = ExecutorContext::new_mock(None).await;
10326 let version = Version(0);
10327
10328 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10329 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10330 let sketch_id = sketch_object.id;
10331 let sketch = expect_sketch(sketch_object);
10332 let line3_id = *sketch.segments.get(8).unwrap();
10333
10334 let (src_delta, scene_delta) = frontend
10335 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10336 .await
10337 .unwrap();
10338 insta::assert_snapshot!(
10339 "test_delete_line_preserves_multiline_parallel_constraint",
10340 src_delta.text.as_str()
10341 );
10342
10343 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10344 let sketch = expect_sketch(sketch_object);
10345 assert_eq!(sketch.constraints.len(), 1);
10346
10347 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10348 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10349 panic!("Expected constraint object");
10350 };
10351 let Constraint::Parallel(parallel) = constraint else {
10352 panic!("Expected parallel constraint");
10353 };
10354 assert_eq!(parallel.lines.len(), 2);
10355
10356 ctx.close().await;
10357 mock_ctx.close().await;
10358 }
10359
10360 #[tokio::test(flavor = "multi_thread")]
10361 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10362 let initial_source = "\
10363sketch(on = XY) {
10364 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10365 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10366 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10367 parallel([line1, line2, line3])
10368}
10369";
10370
10371 let program = Program::parse(initial_source).unwrap().0.unwrap();
10372
10373 let mut frontend = FrontendState::new();
10374
10375 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10376 let mock_ctx = ExecutorContext::new_mock(None).await;
10377 let version = Version(0);
10378
10379 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10380 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10381 let sketch_id = sketch_object.id;
10382 let sketch = expect_sketch(sketch_object);
10383 let line2_id = *sketch.segments.get(5).unwrap();
10384 let line3_id = *sketch.segments.get(8).unwrap();
10385
10386 let (src_delta, scene_delta) = frontend
10387 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10388 .await
10389 .unwrap();
10390 insta::assert_snapshot!(
10391 "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10392 src_delta.text.as_str()
10393 );
10394
10395 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10396 let sketch = expect_sketch(sketch_object);
10397 assert!(sketch.constraints.is_empty());
10398
10399 ctx.close().await;
10400 mock_ctx.close().await;
10401 }
10402
10403 #[tokio::test(flavor = "multi_thread")]
10404 async fn test_delete_line_line_coincident_constraint() {
10405 let initial_source = "\
10406sketch(on = XY) {
10407 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10408 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10409 coincident([line1, line2])
10410}
10411";
10412
10413 let program = Program::parse(initial_source).unwrap().0.unwrap();
10414
10415 let mut frontend = FrontendState::new();
10416
10417 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10418 let mock_ctx = ExecutorContext::new_mock(None).await;
10419 let version = Version(0);
10420
10421 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10422 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10423 let sketch_id = sketch_object.id;
10424 let sketch = expect_sketch(sketch_object);
10425
10426 let coincident_id = *sketch.constraints.first().unwrap();
10427
10428 let (src_delta, scene_delta) = frontend
10429 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10430 .await
10431 .unwrap();
10432 insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10433 assert_eq!(scene_delta.new_objects, vec![]);
10434 assert_eq!(scene_delta.new_graph.objects.len(), 8);
10435
10436 ctx.close().await;
10437 mock_ctx.close().await;
10438 }
10439
10440 #[tokio::test(flavor = "multi_thread")]
10441 async fn test_two_points_coincident() {
10442 let initial_source = "\
10443sketch(on = XY) {
10444 point1 = point(at = [var 1, var 2])
10445 point(at = [3, 4])
10446}
10447";
10448
10449 let program = Program::parse(initial_source).unwrap().0.unwrap();
10450
10451 let mut frontend = FrontendState::new();
10452
10453 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10454 let mock_ctx = ExecutorContext::new_mock(None).await;
10455 let version = Version(0);
10456
10457 frontend.hack_set_program(&ctx, program).await.unwrap();
10458 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10459 let sketch_id = sketch_object.id;
10460 let sketch = expect_sketch(sketch_object);
10461 let point0_id = *sketch.segments.first().unwrap();
10462 let point1_id = *sketch.segments.get(1).unwrap();
10463
10464 let constraint = Constraint::Coincident(Coincident {
10465 segments: vec![point0_id.into(), point1_id.into()],
10466 });
10467 let (src_delta, scene_delta) = frontend
10468 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10469 .await
10470 .unwrap();
10471 insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10472 assert_eq!(
10473 scene_delta.new_graph.objects.len(),
10474 5,
10475 "{:#?}",
10476 scene_delta.new_graph.objects
10477 );
10478
10479 ctx.close().await;
10480 mock_ctx.close().await;
10481 }
10482
10483 #[tokio::test(flavor = "multi_thread")]
10484 async fn test_three_points_coincident() {
10485 let initial_source = "\
10486sketch(on = XY) {
10487 point1 = point(at = [var 1, var 2])
10488 point(at = [var 3, var 4])
10489 point(at = [var 5, var 6])
10490}
10491";
10492
10493 let program = Program::parse(initial_source).unwrap().0.unwrap();
10494
10495 let mut frontend = FrontendState::new();
10496
10497 let mock_ctx = ExecutorContext::new_mock(None).await;
10498 let version = Version(0);
10499
10500 frontend.program = program.clone();
10501 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10502 frontend.update_state_after_exec(outcome, true);
10503 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10504 let sketch_id = sketch_object.id;
10505 let sketch = expect_sketch(sketch_object);
10506 let segments = sketch
10507 .segments
10508 .iter()
10509 .take(3)
10510 .copied()
10511 .map(Into::into)
10512 .collect::<Vec<ConstraintSegment>>();
10513
10514 let constraint = Constraint::Coincident(Coincident {
10515 segments: segments.clone(),
10516 });
10517 let (src_delta, scene_delta) = frontend
10518 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10519 .await
10520 .unwrap();
10521 insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10522
10523 let constraint_object = scene_delta
10524 .new_graph
10525 .objects
10526 .iter()
10527 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10528 .unwrap();
10529
10530 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10531 panic!("expected a constraint object");
10532 };
10533
10534 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10535
10536 mock_ctx.close().await;
10537 }
10538
10539 #[tokio::test(flavor = "multi_thread")]
10540 async fn test_source_with_three_point_coincident_tracks_all_segments() {
10541 let initial_source = "\
10542sketch(on = XY) {
10543 point1 = point(at = [var 1, var 2])
10544 point2 = point(at = [var 3, var 4])
10545 point3 = point(at = [var 5, var 6])
10546 coincident([point1, point2, point3])
10547}
10548";
10549
10550 let program = Program::parse(initial_source).unwrap().0.unwrap();
10551
10552 let mut frontend = FrontendState::new();
10553
10554 let ctx = ExecutorContext::new_mock(None).await;
10555 frontend.program = program.clone();
10556 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10557 frontend.update_state_after_exec(outcome, true);
10558
10559 let constraint_object = frontend
10560 .scene_graph
10561 .objects
10562 .iter()
10563 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10564 .unwrap();
10565 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10566 panic!("expected a constraint object");
10567 };
10568
10569 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10570 let sketch = expect_sketch(sketch_object);
10571 let expected_segments = sketch
10572 .segments
10573 .iter()
10574 .take(3)
10575 .copied()
10576 .map(Into::into)
10577 .collect::<Vec<ConstraintSegment>>();
10578
10579 assert_eq!(
10580 constraint,
10581 &Constraint::Coincident(Coincident {
10582 segments: expected_segments,
10583 })
10584 );
10585
10586 ctx.close().await;
10587 }
10588
10589 #[tokio::test(flavor = "multi_thread")]
10590 async fn test_point_origin_coincident_preserves_order() {
10591 let initial_source = "\
10592sketch(on = XY) {
10593 point(at = [var 1, var 2])
10594}
10595";
10596
10597 for (origin_first, snapshot_name) in [
10598 (true, "test_point_origin_coincident_preserves_order_origin_first"),
10599 (false, "test_point_origin_coincident_preserves_order_point_first"),
10600 ] {
10601 let program = Program::parse(initial_source).unwrap().0.unwrap();
10602
10603 let mut frontend = FrontendState::new();
10604
10605 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10606 let mock_ctx = ExecutorContext::new_mock(None).await;
10607 let version = Version(0);
10608
10609 frontend.hack_set_program(&ctx, program).await.unwrap();
10610 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10611 let sketch_id = sketch_object.id;
10612 let sketch = expect_sketch(sketch_object);
10613 let point_id = *sketch.segments.first().unwrap();
10614
10615 let segments = if origin_first {
10616 vec![ConstraintSegment::ORIGIN, point_id.into()]
10617 } else {
10618 vec![point_id.into(), ConstraintSegment::ORIGIN]
10619 };
10620 let constraint = Constraint::Coincident(Coincident {
10621 segments: segments.clone(),
10622 });
10623 let (src_delta, scene_delta) = frontend
10624 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10625 .await
10626 .unwrap();
10627 insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10628
10629 let constraint_object = scene_delta
10630 .new_graph
10631 .objects
10632 .iter()
10633 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10634 .unwrap();
10635
10636 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10637 panic!("expected a constraint object");
10638 };
10639
10640 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10641
10642 ctx.close().await;
10643 mock_ctx.close().await;
10644 }
10645 }
10646
10647 #[tokio::test(flavor = "multi_thread")]
10648 async fn test_coincident_of_line_end_points() {
10649 let initial_source = "\
10650sketch(on = XY) {
10651 line(start = [var 1, var 2], end = [var 3, var 4])
10652 line(start = [var 5, var 6], end = [var 7, var 8])
10653}
10654";
10655
10656 let program = Program::parse(initial_source).unwrap().0.unwrap();
10657
10658 let mut frontend = FrontendState::new();
10659
10660 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10661 let mock_ctx = ExecutorContext::new_mock(None).await;
10662 let version = Version(0);
10663
10664 frontend.hack_set_program(&ctx, program).await.unwrap();
10665 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10666 let sketch_id = sketch_object.id;
10667 let sketch = expect_sketch(sketch_object);
10668 let point0_id = *sketch.segments.get(1).unwrap();
10669 let point1_id = *sketch.segments.get(3).unwrap();
10670
10671 let constraint = Constraint::Coincident(Coincident {
10672 segments: vec![point0_id.into(), point1_id.into()],
10673 });
10674 let (src_delta, scene_delta) = frontend
10675 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10676 .await
10677 .unwrap();
10678 insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10679 assert_eq!(
10680 scene_delta.new_graph.objects.len(),
10681 9,
10682 "{:#?}",
10683 scene_delta.new_graph.objects
10684 );
10685
10686 ctx.close().await;
10687 mock_ctx.close().await;
10688 }
10689
10690 #[tokio::test(flavor = "multi_thread")]
10691 async fn test_coincident_of_line_point_and_circle_segment() {
10692 let initial_source = "\
10693sketch(on = XY) {
10694 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10695 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10696}
10697";
10698 let program = Program::parse(initial_source).unwrap().0.unwrap();
10699 let mut frontend = FrontendState::new();
10700
10701 let mock_ctx = ExecutorContext::new_mock(None).await;
10702 let version = Version(0);
10703
10704 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10705 frontend.program = program;
10706 frontend.update_state_after_exec(outcome, true);
10707 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10708 let sketch_id = sketch_object.id;
10709 let sketch = expect_sketch(sketch_object);
10710
10711 let circle_id = sketch
10712 .segments
10713 .iter()
10714 .copied()
10715 .find(|seg_id| {
10716 matches!(
10717 &frontend.scene_graph.objects[seg_id.0].kind,
10718 ObjectKind::Segment {
10719 segment: Segment::Circle(_)
10720 }
10721 )
10722 })
10723 .expect("Expected a circle segment in sketch");
10724 let line_id = frontend
10725 .scene_graph
10726 .objects
10727 .iter()
10728 .find_map(|obj| match &obj.kind {
10729 ObjectKind::Segment {
10730 segment: Segment::Line(line),
10731 } if line.owner.is_none() => Some(obj.id),
10732 _ => None,
10733 })
10734 .expect("Expected a standalone line segment in scene graph");
10735
10736 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10737 ObjectKind::Segment {
10738 segment: Segment::Line(line),
10739 } => line.start,
10740 _ => panic!("Expected line segment object"),
10741 };
10742
10743 let constraint = Constraint::Coincident(Coincident {
10744 segments: vec![line_start_point_id.into(), circle_id.into()],
10745 });
10746 let (src_delta, _scene_delta) = frontend
10747 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10748 .await
10749 .unwrap();
10750 insta::assert_snapshot!(
10751 "test_coincident_of_line_point_and_circle_segment",
10752 src_delta.text.as_str()
10753 );
10754
10755 mock_ctx.close().await;
10756 }
10757
10758 #[tokio::test(flavor = "multi_thread")]
10759 async fn test_invalid_coincident_arc_and_line_preserves_state() {
10760 let program = Program::empty();
10768
10769 let mut frontend = FrontendState::new();
10770 frontend.program = program;
10771
10772 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10773 let mock_ctx = ExecutorContext::new_mock(None).await;
10774 let version = Version(0);
10775
10776 let sketch_args = SketchCtor {
10777 on: Plane::Default(PlaneName::Xy),
10778 };
10779 let (_src_delta, _scene_delta, sketch_id) = frontend
10780 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10781 .await
10782 .unwrap();
10783
10784 let arc_ctor = ArcCtor {
10786 start: Point2d {
10787 x: Expr::Var(Number {
10788 value: 0.0,
10789 units: NumericSuffix::Mm,
10790 }),
10791 y: Expr::Var(Number {
10792 value: 0.0,
10793 units: NumericSuffix::Mm,
10794 }),
10795 },
10796 end: Point2d {
10797 x: Expr::Var(Number {
10798 value: 10.0,
10799 units: NumericSuffix::Mm,
10800 }),
10801 y: Expr::Var(Number {
10802 value: 10.0,
10803 units: NumericSuffix::Mm,
10804 }),
10805 },
10806 center: Point2d {
10807 x: Expr::Var(Number {
10808 value: 10.0,
10809 units: NumericSuffix::Mm,
10810 }),
10811 y: Expr::Var(Number {
10812 value: 0.0,
10813 units: NumericSuffix::Mm,
10814 }),
10815 },
10816 direction: None,
10817 construction: None,
10818 };
10819 let (_src_delta, scene_delta) = frontend
10820 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10821 .await
10822 .unwrap();
10823 let arc_id = *scene_delta.new_objects.last().unwrap();
10825
10826 let line_ctor = LineCtor {
10828 start: Point2d {
10829 x: Expr::Var(Number {
10830 value: 20.0,
10831 units: NumericSuffix::Mm,
10832 }),
10833 y: Expr::Var(Number {
10834 value: 0.0,
10835 units: NumericSuffix::Mm,
10836 }),
10837 },
10838 end: Point2d {
10839 x: Expr::Var(Number {
10840 value: 30.0,
10841 units: NumericSuffix::Mm,
10842 }),
10843 y: Expr::Var(Number {
10844 value: 10.0,
10845 units: NumericSuffix::Mm,
10846 }),
10847 },
10848 construction: None,
10849 };
10850 let (_src_delta, scene_delta) = frontend
10851 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10852 .await
10853 .unwrap();
10854 let line_id = *scene_delta.new_objects.last().unwrap();
10856
10857 let constraint = Constraint::Coincident(Coincident {
10860 segments: vec![arc_id.into(), line_id.into()],
10861 });
10862 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10863
10864 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10866
10867 let sketch_object_after =
10870 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10871 let sketch_after = expect_sketch(sketch_object_after);
10872
10873 assert!(
10875 sketch_after.segments.contains(&arc_id),
10876 "Arc segment should still exist after failed constraint"
10877 );
10878 assert!(
10879 sketch_after.segments.contains(&line_id),
10880 "Line segment should still exist after failed constraint"
10881 );
10882
10883 let arc_obj = frontend
10885 .scene_graph
10886 .objects
10887 .get(arc_id.0)
10888 .expect("Arc object should still be accessible");
10889 let line_obj = frontend
10890 .scene_graph
10891 .objects
10892 .get(line_id.0)
10893 .expect("Line object should still be accessible");
10894
10895 match &arc_obj.kind {
10898 ObjectKind::Segment {
10899 segment: Segment::Arc(_),
10900 } => {}
10901 _ => panic!("Arc object should still be an arc segment"),
10902 }
10903 match &line_obj.kind {
10904 ObjectKind::Segment {
10905 segment: Segment::Line(_),
10906 } => {}
10907 _ => panic!("Line object should still be a line segment"),
10908 }
10909
10910 ctx.close().await;
10911 mock_ctx.close().await;
10912 }
10913
10914 #[tokio::test(flavor = "multi_thread")]
10915 async fn test_distance_two_points() {
10916 let initial_source = "\
10917sketch(on = XY) {
10918 point(at = [var 1, var 2])
10919 point(at = [var 3, var 4])
10920}
10921";
10922
10923 let program = Program::parse(initial_source).unwrap().0.unwrap();
10924
10925 let mut frontend = FrontendState::new();
10926
10927 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10928 let mock_ctx = ExecutorContext::new_mock(None).await;
10929 let version = Version(0);
10930
10931 frontend.hack_set_program(&ctx, program).await.unwrap();
10932 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10933 let sketch_id = sketch_object.id;
10934 let sketch = expect_sketch(sketch_object);
10935 let point0_id = *sketch.segments.first().unwrap();
10936 let point1_id = *sketch.segments.get(1).unwrap();
10937
10938 let constraint = Constraint::Distance(Distance {
10939 segments: vec![point0_id.into(), point1_id.into()],
10940 distance: Number {
10941 value: 2.0,
10942 units: NumericSuffix::Mm,
10943 },
10944 label_position: None,
10945 source: Default::default(),
10946 });
10947 let (src_delta, scene_delta) = frontend
10948 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10949 .await
10950 .unwrap();
10951 insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
10952 assert_eq!(
10953 scene_delta.new_graph.objects.len(),
10954 5,
10955 "{:#?}",
10956 scene_delta.new_graph.objects
10957 );
10958
10959 ctx.close().await;
10960 mock_ctx.close().await;
10961 }
10962
10963 #[tokio::test(flavor = "multi_thread")]
10964 async fn test_distance_two_points_with_label() {
10965 let initial_source = "\
10966sketch(on = XY) {
10967 point(at = [var 1, var 2])
10968 point(at = [var 3, var 4])
10969}
10970";
10971
10972 let program = Program::parse(initial_source).unwrap().0.unwrap();
10973
10974 let mut frontend = FrontendState::new();
10975
10976 let mock_ctx = ExecutorContext::new_mock(None).await;
10977 let version = Version(0);
10978
10979 frontend.program = program.clone();
10980 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10981 frontend.update_state_after_exec(outcome, true);
10982 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10983 let sketch_id = sketch_object.id;
10984 let sketch = expect_sketch(sketch_object);
10985 let point0_id = *sketch.segments.first().unwrap();
10986 let point1_id = *sketch.segments.get(1).unwrap();
10987
10988 let label_position = Point2d {
10989 x: Number {
10990 value: 10.0,
10991 units: NumericSuffix::Mm,
10992 },
10993 y: Number {
10994 value: 11.0,
10995 units: NumericSuffix::Mm,
10996 },
10997 };
10998 let constraint = Constraint::Distance(Distance {
10999 segments: vec![point0_id.into(), point1_id.into()],
11000 distance: Number {
11001 value: 2.0,
11002 units: NumericSuffix::Mm,
11003 },
11004 label_position: Some(label_position.clone()),
11005 source: Default::default(),
11006 });
11007 let (src_delta, scene_delta) = frontend
11008 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11009 .await
11010 .unwrap();
11011 insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11012
11013 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11014 let sketch = expect_sketch(sketch_object);
11015 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11016 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11017 panic!("Expected constraint object");
11018 };
11019 let Constraint::Distance(distance) = constraint else {
11020 panic!("Expected distance constraint");
11021 };
11022 assert_eq!(distance.label_position, Some(label_position));
11023
11024 mock_ctx.close().await;
11025 }
11026
11027 #[tokio::test(flavor = "multi_thread")]
11028 async fn test_edit_distance_constraint_label_position() {
11029 let initial_source = "\
11030sketch(on = XY) {
11031 point(at = [var 1, var 2])
11032 point(at = [var 3, var 2])
11033}
11034";
11035
11036 let program = Program::parse(initial_source).unwrap().0.unwrap();
11037
11038 let mut frontend = FrontendState::new();
11039
11040 let mock_ctx = ExecutorContext::new_mock(None).await;
11041 let version = Version(0);
11042
11043 frontend.program = program.clone();
11044 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11045 frontend.update_state_after_exec(outcome, true);
11046 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11047 let sketch_id = sketch_object.id;
11048 let sketch = expect_sketch(sketch_object);
11049 let point0_id = *sketch.segments.first().unwrap();
11050 let point1_id = *sketch.segments.get(1).unwrap();
11051
11052 let constraint = Constraint::Distance(Distance {
11053 segments: vec![point0_id.into(), point1_id.into()],
11054 distance: Number {
11055 value: 2.0,
11056 units: NumericSuffix::Mm,
11057 },
11058 label_position: None,
11059 source: Default::default(),
11060 });
11061 let (_, scene_delta) = frontend
11062 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11063 .await
11064 .unwrap();
11065 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11066 let sketch = expect_sketch(sketch_object);
11067 let constraint_id = sketch.constraints[0];
11068 let label_position = Point2d {
11069 x: Number {
11070 value: 10.0,
11071 units: NumericSuffix::Mm,
11072 },
11073 y: Number {
11074 value: 11.0,
11075 units: NumericSuffix::Mm,
11076 },
11077 };
11078
11079 let (src_delta, scene_delta) = frontend
11080 .edit_distance_constraint_label_position(
11081 &mock_ctx,
11082 version,
11083 sketch_id,
11084 constraint_id,
11085 label_position.clone(),
11086 vec![],
11087 )
11088 .await
11089 .unwrap();
11090 insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11091
11092 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11093 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11094 panic!("Expected constraint object");
11095 };
11096 let Constraint::Distance(distance) = constraint else {
11097 panic!("Expected distance constraint");
11098 };
11099 assert_eq!(distance.label_position, Some(label_position));
11100
11101 mock_ctx.close().await;
11102 }
11103
11104 #[tokio::test(flavor = "multi_thread")]
11105 async fn test_edit_distance_constraint_type_and_value() {
11106 let initial_source = "\
11107sketch(on = XY) {
11108 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11109 distance([line1.start, line1.end]) == 5mm
11110}
11111";
11112
11113 let program = Program::parse(initial_source).unwrap().0.unwrap();
11114 let mut frontend = FrontendState::new();
11115 let mock_ctx = ExecutorContext::new_mock(None).await;
11116 let version = Version(0);
11117
11118 frontend.program = program.clone();
11119 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11120 frontend.update_state_after_exec(outcome, true);
11121 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11122 let sketch_id = sketch_object.id;
11123 let sketch = expect_sketch(sketch_object);
11124 let constraint_id = sketch.constraints[0];
11125 let point0_id = sketch.segments[0];
11126 let point1_id = sketch.segments[1];
11127 let label_position = Point2d {
11128 x: Number {
11129 value: 2.0,
11130 units: NumericSuffix::Mm,
11131 },
11132 y: Number {
11133 value: 5.0,
11134 units: NumericSuffix::Mm,
11135 },
11136 };
11137
11138 let (source_delta, scene_delta) = frontend
11139 .edit_distance_constraint_with_options(
11140 &mock_ctx,
11141 version,
11142 sketch_id,
11143 constraint_id,
11144 Constraint::HorizontalDistance(Distance {
11145 segments: vec![point0_id.into(), point1_id.into()],
11146 distance: Number {
11147 value: 4.0,
11148 units: NumericSuffix::Mm,
11149 },
11150 label_position: Some(label_position.clone()),
11151 source: Default::default(),
11152 }),
11153 EditConstraintOptions {
11154 commit_solved_initial_guesses: false,
11155 },
11156 )
11157 .await
11158 .unwrap();
11159 assert_eq!(
11160 source_delta.text,
11161 "\
11162sketch(on = XY) {
11163 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11164 horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11165}
11166"
11167 );
11168
11169 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11170 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11171 panic!("Expected constraint object");
11172 };
11173 let Constraint::HorizontalDistance(distance) = constraint else {
11174 panic!("Expected horizontal distance constraint");
11175 };
11176 assert_eq!(distance.distance.value, 4.0);
11177 assert_eq!(distance.label_position, Some(label_position));
11178
11179 mock_ctx.close().await;
11180 }
11181
11182 #[tokio::test(flavor = "multi_thread")]
11183 async fn test_edit_angle_constraint_label_position() {
11184 let initial_source = "\
11185sketch(on = XY) {
11186 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11187 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11188 angle([line1, line2]) == 60deg
11189}
11190";
11191
11192 let program = Program::parse(initial_source).unwrap().0.unwrap();
11193 let mut frontend = FrontendState::new();
11194 let mock_ctx = ExecutorContext::new_mock(None).await;
11195 let version = Version(0);
11196
11197 frontend.program = program.clone();
11198 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11199 frontend.update_state_after_exec(outcome, true);
11200 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11201 let sketch_id = sketch_object.id;
11202 let sketch = expect_sketch(sketch_object);
11203 let constraint_id = sketch.constraints[0];
11204 let label_position = Point2d {
11205 x: Number {
11206 value: 10.0,
11207 units: NumericSuffix::Mm,
11208 },
11209 y: Number {
11210 value: 11.0,
11211 units: NumericSuffix::Mm,
11212 },
11213 };
11214
11215 let (src_delta, scene_delta) = frontend
11216 .edit_distance_constraint_label_position(
11217 &mock_ctx,
11218 version,
11219 sketch_id,
11220 constraint_id,
11221 label_position.clone(),
11222 vec![],
11223 )
11224 .await
11225 .unwrap();
11226 assert_eq!(
11227 src_delta.text.as_str(),
11228 "\
11229sketch(on = XY) {
11230 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11231 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11232 angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11233}
11234"
11235 );
11236
11237 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11238 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11239 panic!("Expected constraint object");
11240 };
11241 let Constraint::Angle(angle) = constraint else {
11242 panic!("Expected angle constraint");
11243 };
11244 assert_eq!(angle.label_position, Some(label_position));
11245
11246 mock_ctx.close().await;
11247 }
11248
11249 #[tokio::test(flavor = "multi_thread")]
11250 async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11251 let initial_source = "\
11252sketch(on = XY) {
11253 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11254 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11255 60deg == angleDimension(lines = [line1, line2], sector = 1)
11256}
11257";
11258
11259 let program = Program::parse(initial_source).unwrap().0.unwrap();
11260 let mut frontend = FrontendState::new();
11261 let mock_ctx = ExecutorContext::new_mock(None).await;
11262 let version = Version(0);
11263
11264 frontend.program = program.clone();
11265 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11266 frontend.update_state_after_exec(outcome, true);
11267 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11268 let sketch_id = sketch_object.id;
11269 let sketch = expect_sketch(sketch_object);
11270 let constraint_id = sketch.constraints[0];
11271 let label_position = Point2d {
11272 x: Number {
11273 value: 10.0,
11274 units: NumericSuffix::Mm,
11275 },
11276 y: Number {
11277 value: 11.0,
11278 units: NumericSuffix::Mm,
11279 },
11280 };
11281
11282 let (src_delta, scene_delta) = frontend
11283 .edit_distance_constraint_label_position(
11284 &mock_ctx,
11285 version,
11286 sketch_id,
11287 constraint_id,
11288 label_position.clone(),
11289 vec![],
11290 )
11291 .await
11292 .unwrap();
11293 assert_eq!(
11294 src_delta.text.as_str(),
11295 "\
11296sketch(on = XY) {
11297 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11298 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11299 60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11300}
11301"
11302 );
11303
11304 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11305 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11306 panic!("Expected constraint object");
11307 };
11308 let Constraint::Angle(angle) = constraint else {
11309 panic!("Expected angle constraint");
11310 };
11311 assert_eq!(angle.label_position, Some(label_position));
11312
11313 mock_ctx.close().await;
11314 }
11315
11316 #[tokio::test(flavor = "multi_thread")]
11317 async fn test_edit_angle_constraint() {
11318 let initial_source = "\
11319sketch(on = XY) {
11320 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11321 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11322 angle([line1, line2]) == 60deg
11323}
11324";
11325
11326 let program = Program::parse(initial_source).unwrap().0.unwrap();
11327 let mut frontend = FrontendState::new();
11328 let mock_ctx = ExecutorContext::new_mock(None).await;
11329 let version = Version(0);
11330
11331 frontend.program = program.clone();
11332 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11333 frontend.update_state_after_exec(outcome, true);
11334 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11335 let sketch_id = sketch_object.id;
11336 let sketch = expect_sketch(sketch_object);
11337 let constraint_id = sketch.constraints[0];
11338 let line1_id = *sketch.segments.get(2).unwrap();
11339 let line2_id = *sketch.segments.get(5).unwrap();
11340 let label_position = Point2d {
11341 x: Number {
11342 value: 10.0,
11343 units: NumericSuffix::Mm,
11344 },
11345 y: Number {
11346 value: 11.0,
11347 units: NumericSuffix::Mm,
11348 },
11349 };
11350
11351 let (src_delta, scene_delta) = frontend
11352 .edit_angle_constraint_with_options(
11353 &mock_ctx,
11354 version,
11355 sketch_id,
11356 constraint_id,
11357 Angle {
11358 lines: vec![line2_id, line1_id],
11359 angle: Number {
11360 value: 60.0,
11361 units: NumericSuffix::Deg,
11362 },
11363 sector: Some(3),
11364 inverse: Some(false),
11365 label_position: Some(label_position.clone()),
11366 source: Default::default(),
11367 },
11368 EditConstraintOptions {
11369 commit_solved_initial_guesses: false,
11370 },
11371 )
11372 .await
11373 .unwrap();
11374 assert_eq!(
11375 src_delta.text.as_str(),
11376 "\
11377sketch(on = XY) {
11378 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11379 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11380 angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11381}
11382"
11383 );
11384
11385 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11386 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11387 panic!("Expected constraint object");
11388 };
11389 let Constraint::Angle(angle) = constraint else {
11390 panic!("Expected angle constraint");
11391 };
11392 assert_eq!(angle.lines, vec![line2_id, line1_id]);
11393 assert_eq!(angle.sector, Some(3));
11394 assert_eq!(angle.inverse, Some(false));
11395 assert_eq!(angle.label_position, Some(label_position));
11396
11397 mock_ctx.close().await;
11398 }
11399
11400 #[tokio::test(flavor = "multi_thread")]
11401 async fn test_edit_angle_constraint_with_call_on_right() {
11402 let initial_source = "\
11403sketch(on = XY) {
11404 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11405 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11406 60deg == angle([line1, line2])
11407}
11408";
11409
11410 let program = Program::parse(initial_source).unwrap().0.unwrap();
11411 let mut frontend = FrontendState::new();
11412 let mock_ctx = ExecutorContext::new_mock(None).await;
11413 let version = Version(0);
11414
11415 frontend.program = program.clone();
11416 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11417 frontend.update_state_after_exec(outcome, true);
11418 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11419 let sketch_id = sketch_object.id;
11420 let sketch = expect_sketch(sketch_object);
11421 let constraint_id = sketch.constraints[0];
11422 let line1_id = *sketch.segments.get(2).unwrap();
11423 let line2_id = *sketch.segments.get(5).unwrap();
11424
11425 let (src_delta, _) = frontend
11426 .edit_angle_constraint_with_options(
11427 &mock_ctx,
11428 version,
11429 sketch_id,
11430 constraint_id,
11431 Angle {
11432 lines: vec![line2_id, line1_id],
11433 angle: Number {
11434 value: 60.0,
11435 units: NumericSuffix::Deg,
11436 },
11437 sector: Some(3),
11438 inverse: Some(false),
11439 label_position: None,
11440 source: Default::default(),
11441 },
11442 EditConstraintOptions {
11443 commit_solved_initial_guesses: false,
11444 },
11445 )
11446 .await
11447 .unwrap();
11448 assert_eq!(
11449 src_delta.text.as_str(),
11450 "\
11451sketch(on = XY) {
11452 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11453 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11454 60deg == angleDimension(lines = [line2, line1], sector = 3)
11455}
11456"
11457 );
11458
11459 mock_ctx.close().await;
11460 }
11461
11462 #[tokio::test(flavor = "multi_thread")]
11463 async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11464 let initial_source = "\
11465@settings(kclVersion = 2.0)
11466
11467sketch001 = sketch(on = XZ) {
11468 line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11469 line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11470 distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11471 vertical([line1.start, ORIGIN])
11472}
11473";
11474
11475 let program = Program::parse(initial_source).unwrap().0.unwrap();
11476 let mut frontend = FrontendState::new();
11477 let mock_ctx = ExecutorContext::new_mock(None).await;
11478 let version = Version(0);
11479
11480 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11481 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11482 let sketch_id = sketch_object.id;
11483 let sketch = expect_sketch(sketch_object);
11484 let constraint_id = sketch
11485 .constraints
11486 .iter()
11487 .copied()
11488 .find(|constraint_id| {
11489 matches!(
11490 frontend.scene_graph.objects[constraint_id.0].kind,
11491 ObjectKind::Constraint {
11492 constraint: Constraint::Distance(_)
11493 }
11494 )
11495 })
11496 .unwrap();
11497 let line1_id = sketch
11498 .segments
11499 .iter()
11500 .copied()
11501 .find(|segment_id| {
11502 matches!(
11503 frontend.scene_graph.objects[segment_id.0].kind,
11504 ObjectKind::Segment {
11505 segment: Segment::Line(_)
11506 }
11507 )
11508 })
11509 .unwrap();
11510 let label_position = Point2d {
11511 x: Number {
11512 value: 7.0,
11513 units: NumericSuffix::Mm,
11514 },
11515 y: Number {
11516 value: 9.0,
11517 units: NumericSuffix::Mm,
11518 },
11519 };
11520
11521 let (source_delta, scene_delta) = frontend
11522 .edit_segments_with_options(
11523 &mock_ctx,
11524 version,
11525 sketch_id,
11526 vec![ExistingSegmentCtor {
11527 id: line1_id,
11528 ctor: SegmentCtor::Line(LineCtor {
11529 start: point_expr_mm(2.0, 15.55),
11530 end: point_expr_mm(-4.03, 11.51),
11531 construction: None,
11532 }),
11533 }],
11534 EditSegmentsOptions {
11535 anchor_segment_ids: Some(vec![]),
11536 drag_anchors: vec![SegmentDragAnchor {
11537 segment_id: line1_id,
11538 target: label_position.clone(),
11539 }],
11540 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11541 constraint_id,
11542 label_position: label_position.clone(),
11543 }],
11544 commit_solved_initial_guesses: true,
11545 },
11546 )
11547 .await
11548 .unwrap();
11549
11550 assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11551 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11552 let ObjectKind::Constraint {
11553 constraint: Constraint::Distance(distance),
11554 } = &constraint_object.kind
11555 else {
11556 panic!("Expected distance constraint object");
11557 };
11558 assert_eq!(distance.label_position, Some(label_position));
11559
11560 let snapped_label_position = Point2d {
11561 x: Number {
11562 value: 8.0,
11563 units: NumericSuffix::Mm,
11564 },
11565 y: Number {
11566 value: 10.0,
11567 units: NumericSuffix::Mm,
11568 },
11569 };
11570 let (source_delta, scene_delta) = frontend
11571 .edit_segments_with_options(
11572 &mock_ctx,
11573 version,
11574 sketch_id,
11575 vec![],
11576 EditSegmentsOptions {
11577 anchor_segment_ids: Some(vec![line1_id]),
11578 drag_anchors: vec![],
11579 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11580 constraint_id,
11581 label_position: snapped_label_position.clone(),
11582 }],
11583 commit_solved_initial_guesses: true,
11584 },
11585 )
11586 .await
11587 .unwrap();
11588
11589 assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11590 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11591 let ObjectKind::Constraint {
11592 constraint: Constraint::Distance(distance),
11593 } = &constraint_object.kind
11594 else {
11595 panic!("Expected distance constraint object");
11596 };
11597 assert_eq!(distance.label_position, Some(snapped_label_position));
11598
11599 mock_ctx.close().await;
11600 }
11601
11602 #[tokio::test(flavor = "multi_thread")]
11603 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11604 let initial_source = "\
11605sketch(on = XY) {
11606 point1 = point(at = [var 0mm, var 0mm])
11607 point2 = point(at = [var 10mm, var 0mm])
11608 distance([point1, point2]) == 5mm
11609}
11610";
11611
11612 let program = Program::parse(initial_source).unwrap().0.unwrap();
11613 let mut frontend = FrontendState::new();
11614 let mock_ctx = ExecutorContext::new_mock(None).await;
11615 let version = Version(0);
11616
11617 frontend.program = program.clone();
11618 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11619 frontend.update_state_after_exec(outcome, true);
11620 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11621 let sketch_id = sketch_object.id;
11622 let sketch = expect_sketch(sketch_object);
11623 let point0_id = sketch.segments[0];
11624 let point1_id = sketch.segments[1];
11625 let constraint_id = sketch.constraints[0];
11626
11627 let edited_segments = vec![ExistingSegmentCtor {
11628 id: point0_id,
11629 ctor: SegmentCtor::Point(PointCtor {
11630 position: Point2d {
11631 x: Expr::Var(Number {
11632 value: 2.0,
11633 units: NumericSuffix::Mm,
11634 }),
11635 y: Expr::Var(Number {
11636 value: 1.0,
11637 units: NumericSuffix::Mm,
11638 }),
11639 },
11640 }),
11641 }];
11642 let (_, scene_delta) = frontend
11643 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11644 .await
11645 .unwrap();
11646 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11647 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11648
11649 let label_position = Point2d {
11650 x: Number {
11651 value: 3.0,
11652 units: NumericSuffix::Mm,
11653 },
11654 y: Number {
11655 value: 4.0,
11656 units: NumericSuffix::Mm,
11657 },
11658 };
11659 let (_, scene_delta) = frontend
11660 .edit_distance_constraint_label_position(
11661 &mock_ctx,
11662 version,
11663 sketch_id,
11664 constraint_id,
11665 label_position,
11666 vec![point0_id],
11667 )
11668 .await
11669 .unwrap();
11670
11671 assert_point_position_close(
11672 point_position(&scene_delta.new_graph, point0_id),
11673 point0_after_segment_edit,
11674 );
11675 assert_point_position_close(
11676 point_position(&scene_delta.new_graph, point1_id),
11677 point1_after_segment_edit,
11678 );
11679
11680 mock_ctx.close().await;
11681 }
11682
11683 #[tokio::test(flavor = "multi_thread")]
11684 async fn test_distance_point_line() {
11685 let initial_source = "\
11686sketch(on = XY) {
11687 point(at = [var 0, var 5])
11688 line(start = [var 0, var 0], end = [var 10, var 0])
11689}
11690";
11691
11692 let program = Program::parse(initial_source).unwrap().0.unwrap();
11693
11694 let mut frontend = FrontendState::new();
11695
11696 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11697 let mock_ctx = ExecutorContext::new_mock(None).await;
11698 let version = Version(0);
11699
11700 frontend.hack_set_program(&ctx, program).await.unwrap();
11701 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11702 let sketch_id = sketch_object.id;
11703 let sketch = expect_sketch(sketch_object);
11704 let point_id = *sketch.segments.first().unwrap();
11705 let line_id = *sketch
11706 .segments
11707 .iter()
11708 .find(|segment_id| {
11709 matches!(
11710 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11711 Some(ObjectKind::Segment {
11712 segment: Segment::Line(_)
11713 })
11714 )
11715 })
11716 .unwrap();
11717
11718 let label_position = Point2d {
11719 x: Number {
11720 value: 10.0,
11721 units: NumericSuffix::Mm,
11722 },
11723 y: Number {
11724 value: 11.0,
11725 units: NumericSuffix::Mm,
11726 },
11727 };
11728 let constraint = Constraint::Distance(Distance {
11729 segments: vec![point_id.into(), line_id.into()],
11730 distance: Number {
11731 value: 5.0,
11732 units: NumericSuffix::Mm,
11733 },
11734 label_position: Some(label_position.clone()),
11735 source: Default::default(),
11736 });
11737 let (src_delta, scene_delta) = frontend
11738 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11739 .await
11740 .unwrap();
11741 insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11742 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11743 let sketch = expect_sketch(sketch_object);
11744 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11745 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11746 panic!("Expected constraint object");
11747 };
11748 let Constraint::Distance(distance) = constraint else {
11749 panic!("Expected distance constraint");
11750 };
11751 assert_eq!(distance.label_position, Some(label_position));
11752
11753 ctx.close().await;
11754 mock_ctx.close().await;
11755 }
11756
11757 #[tokio::test(flavor = "multi_thread")]
11758 async fn test_distance_point_arc() {
11759 let initial_source = "\
11760sketch(on = XY) {
11761 point(at = [var 0, var 8])
11762 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11763}
11764";
11765
11766 let program = Program::parse(initial_source).unwrap().0.unwrap();
11767
11768 let mut frontend = FrontendState::new();
11769
11770 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11771 let mock_ctx = ExecutorContext::new_mock(None).await;
11772 let version = Version(0);
11773
11774 frontend.hack_set_program(&ctx, program).await.unwrap();
11775 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11776 let sketch_id = sketch_object.id;
11777 let sketch = expect_sketch(sketch_object);
11778 let point_id = *sketch.segments.first().unwrap();
11779 let arc_id = *sketch
11780 .segments
11781 .iter()
11782 .find(|segment_id| {
11783 matches!(
11784 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11785 Some(ObjectKind::Segment {
11786 segment: Segment::Arc(_)
11787 })
11788 )
11789 })
11790 .unwrap();
11791
11792 let constraint = Constraint::Distance(Distance {
11793 segments: vec![point_id.into(), arc_id.into()],
11794 distance: Number {
11795 value: 3.0,
11796 units: NumericSuffix::Mm,
11797 },
11798 label_position: None,
11799 source: Default::default(),
11800 });
11801 let (src_delta, _scene_delta) = frontend
11802 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11803 .await
11804 .unwrap();
11805 insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11806
11807 ctx.close().await;
11808 mock_ctx.close().await;
11809 }
11810
11811 #[tokio::test(flavor = "multi_thread")]
11812 async fn test_distance_arc_origin() {
11813 let initial_source = "\
11814sketch001 = sketch(on = XY) {
11815 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11816}
11817";
11818
11819 let program = Program::parse(initial_source).unwrap().0.unwrap();
11820
11821 let mut frontend = FrontendState::new();
11822
11823 let mock_ctx = ExecutorContext::new_mock(None).await;
11824 let version = Version(0);
11825
11826 frontend.program = program.clone();
11827 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11828 frontend.update_state_after_exec(outcome, true);
11829 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11830 let sketch_id = sketch_object.id;
11831 let sketch = expect_sketch(sketch_object);
11832 let arc_id = *sketch
11833 .segments
11834 .iter()
11835 .find(|segment_id| {
11836 matches!(
11837 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11838 Some(ObjectKind::Segment {
11839 segment: Segment::Arc(_)
11840 })
11841 )
11842 })
11843 .unwrap();
11844
11845 let constraint = Constraint::Distance(Distance {
11846 segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11847 distance: Number {
11848 value: 3.0,
11849 units: NumericSuffix::Mm,
11850 },
11851 label_position: None,
11852 source: Default::default(),
11853 });
11854 let (src_delta, _scene_delta) = frontend
11855 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11856 .await
11857 .unwrap();
11858 insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11859
11860 mock_ctx.close().await;
11861 }
11862
11863 #[tokio::test(flavor = "multi_thread")]
11864 async fn test_distance_line_origin() {
11865 let initial_source = "\
11866sketch(on = XY) {
11867 line(start = [var 5, var 0], end = [var 5, var 10])
11868}
11869";
11870
11871 let program = Program::parse(initial_source).unwrap().0.unwrap();
11872
11873 let mut frontend = FrontendState::new();
11874
11875 let mock_ctx = ExecutorContext::new_mock(None).await;
11876 let version = Version(0);
11877
11878 frontend.program = program.clone();
11879 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11880 frontend.update_state_after_exec(outcome, true);
11881 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11882 let sketch_id = sketch_object.id;
11883 let sketch = expect_sketch(sketch_object);
11884 let line_id = *sketch
11885 .segments
11886 .iter()
11887 .find(|segment_id| {
11888 matches!(
11889 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11890 Some(ObjectKind::Segment {
11891 segment: Segment::Line(_)
11892 })
11893 )
11894 })
11895 .unwrap();
11896
11897 let constraint = Constraint::Distance(Distance {
11898 segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11899 distance: Number {
11900 value: 5.0,
11901 units: NumericSuffix::Mm,
11902 },
11903 label_position: None,
11904 source: Default::default(),
11905 });
11906 let (src_delta, _scene_delta) = frontend
11907 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11908 .await
11909 .unwrap();
11910 insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11911
11912 mock_ctx.close().await;
11913 }
11914
11915 #[tokio::test(flavor = "multi_thread")]
11916 async fn test_distance_line_circle() {
11917 let initial_source = "\
11918sketch(on = XY) {
11919 line(start = [var -10, var 8], end = [var 10, var 8])
11920 circle(start = [var 5, var 0], center = [var 0, var 0])
11921}
11922";
11923
11924 let program = Program::parse(initial_source).unwrap().0.unwrap();
11925
11926 let mut frontend = FrontendState::new();
11927
11928 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11929 let mock_ctx = ExecutorContext::new_mock(None).await;
11930 let version = Version(0);
11931
11932 frontend.hack_set_program(&ctx, program).await.unwrap();
11933 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11934 let sketch_id = sketch_object.id;
11935 let sketch = expect_sketch(sketch_object);
11936 let line_id = *sketch
11937 .segments
11938 .iter()
11939 .find(|segment_id| {
11940 matches!(
11941 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11942 Some(ObjectKind::Segment {
11943 segment: Segment::Line(_)
11944 })
11945 )
11946 })
11947 .unwrap();
11948 let circle_id = *sketch
11949 .segments
11950 .iter()
11951 .find(|segment_id| {
11952 matches!(
11953 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11954 Some(ObjectKind::Segment {
11955 segment: Segment::Circle(_)
11956 })
11957 )
11958 })
11959 .unwrap();
11960
11961 let constraint = Constraint::Distance(Distance {
11962 segments: vec![line_id.into(), circle_id.into()],
11963 distance: Number {
11964 value: 3.0,
11965 units: NumericSuffix::Mm,
11966 },
11967 label_position: None,
11968 source: Default::default(),
11969 });
11970 let (src_delta, _scene_delta) = frontend
11971 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11972 .await
11973 .unwrap();
11974 insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
11975
11976 ctx.close().await;
11977 mock_ctx.close().await;
11978 }
11979
11980 #[tokio::test(flavor = "multi_thread")]
11981 async fn test_distance_circle_arc() {
11982 let initial_source = "\
11983sketch(on = XY) {
11984 circle(start = [var 5, var 0], center = [var 0, var 0])
11985 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11986}
11987";
11988
11989 let program = Program::parse(initial_source).unwrap().0.unwrap();
11990
11991 let mut frontend = FrontendState::new();
11992
11993 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11994 let mock_ctx = ExecutorContext::new_mock(None).await;
11995 let version = Version(0);
11996
11997 frontend.hack_set_program(&ctx, program).await.unwrap();
11998 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11999 let sketch_id = sketch_object.id;
12000 let sketch = expect_sketch(sketch_object);
12001 let circle_id = *sketch
12002 .segments
12003 .iter()
12004 .find(|segment_id| {
12005 matches!(
12006 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12007 Some(ObjectKind::Segment {
12008 segment: Segment::Circle(_)
12009 })
12010 )
12011 })
12012 .unwrap();
12013 let arc_id = *sketch
12014 .segments
12015 .iter()
12016 .find(|segment_id| {
12017 matches!(
12018 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12019 Some(ObjectKind::Segment {
12020 segment: Segment::Arc(_)
12021 })
12022 )
12023 })
12024 .unwrap();
12025
12026 let constraint = Constraint::Distance(Distance {
12027 segments: vec![circle_id.into(), arc_id.into()],
12028 distance: Number {
12029 value: 3.0,
12030 units: NumericSuffix::Mm,
12031 },
12032 label_position: None,
12033 source: Default::default(),
12034 });
12035 let (src_delta, _scene_delta) = frontend
12036 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12037 .await
12038 .unwrap();
12039 insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12040
12041 ctx.close().await;
12042 mock_ctx.close().await;
12043 }
12044
12045 #[tokio::test(flavor = "multi_thread")]
12046 async fn test_distance_parallel_lines() {
12047 let initial_source = "\
12048sketch(on = XY) {
12049 line(start = [var 0, var 0], end = [var 10, var 0])
12050 line(start = [var 0, var 5], end = [var 10, var 5])
12051}
12052";
12053
12054 let program = Program::parse(initial_source).unwrap().0.unwrap();
12055
12056 let mut frontend = FrontendState::new();
12057
12058 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12059 let mock_ctx = ExecutorContext::new_mock(None).await;
12060 let version = Version(0);
12061
12062 frontend.hack_set_program(&ctx, program).await.unwrap();
12063 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12064 let sketch_id = sketch_object.id;
12065 let sketch = expect_sketch(sketch_object);
12066 let line_ids = sketch
12067 .segments
12068 .iter()
12069 .copied()
12070 .filter(|segment_id| {
12071 matches!(
12072 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12073 Some(ObjectKind::Segment {
12074 segment: Segment::Line(_)
12075 })
12076 )
12077 })
12078 .collect::<Vec<_>>();
12079
12080 let constraint = Constraint::Distance(Distance {
12081 segments: vec![line_ids[0].into(), line_ids[1].into()],
12082 distance: Number {
12083 value: 5.0,
12084 units: NumericSuffix::Mm,
12085 },
12086 label_position: None,
12087 source: Default::default(),
12088 });
12089 let (src_delta, _scene_delta) = frontend
12090 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12091 .await
12092 .unwrap();
12093 insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12094
12095 ctx.close().await;
12096 mock_ctx.close().await;
12097 }
12098
12099 #[tokio::test(flavor = "multi_thread")]
12100 async fn test_distance_non_parallel_lines_lowers_to_distance() {
12101 let initial_source = "\
12106sketch(on = XY) {
12107 line(start = [var 0, var 0], end = [var 10, var 0])
12108 line(start = [var 0, var 0], end = [var 10, var 10])
12109}
12110";
12111
12112 let program = Program::parse(initial_source).unwrap().0.unwrap();
12113
12114 let mut frontend = FrontendState::new();
12115
12116 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12117 let mock_ctx = ExecutorContext::new_mock(None).await;
12118 let version = Version(0);
12119
12120 frontend.hack_set_program(&ctx, program).await.unwrap();
12121 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12122 let sketch_id = sketch_object.id;
12123 let sketch = expect_sketch(sketch_object);
12124 let line_ids = sketch
12125 .segments
12126 .iter()
12127 .copied()
12128 .filter(|segment_id| {
12129 matches!(
12130 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12131 Some(ObjectKind::Segment {
12132 segment: Segment::Line(_)
12133 })
12134 )
12135 })
12136 .collect::<Vec<_>>();
12137
12138 let constraint = Constraint::Distance(Distance {
12139 segments: vec![line_ids[0].into(), line_ids[1].into()],
12140 distance: Number {
12141 value: 5.0,
12142 units: NumericSuffix::Mm,
12143 },
12144 label_position: None,
12145 source: Default::default(),
12146 });
12147 let (src_delta, _scene_delta) = frontend
12148 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12149 .await
12150 .unwrap();
12151 insta::assert_snapshot!(
12152 "test_distance_non_parallel_lines_lowers_to_distance",
12153 src_delta.text.as_str()
12154 );
12155
12156 ctx.close().await;
12157 mock_ctx.close().await;
12158 }
12159
12160 #[tokio::test(flavor = "multi_thread")]
12161 async fn test_horizontal_distance_two_points() {
12162 let initial_source = "\
12163sketch(on = XY) {
12164 point(at = [var 1, var 2])
12165 point(at = [var 3, var 4])
12166}
12167";
12168
12169 let program = Program::parse(initial_source).unwrap().0.unwrap();
12170
12171 let mut frontend = FrontendState::new();
12172
12173 let mock_ctx = ExecutorContext::new_mock(None).await;
12174 let version = Version(0);
12175
12176 frontend.program = program.clone();
12177 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12178 frontend.update_state_after_exec(outcome, true);
12179 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12180 let sketch_id = sketch_object.id;
12181 let sketch = expect_sketch(sketch_object);
12182 let point0_id = *sketch.segments.first().unwrap();
12183 let point1_id = *sketch.segments.get(1).unwrap();
12184 let label_position = Point2d {
12185 x: Number {
12186 value: 10.0,
12187 units: NumericSuffix::Mm,
12188 },
12189 y: Number {
12190 value: 11.0,
12191 units: NumericSuffix::Mm,
12192 },
12193 };
12194
12195 let constraint = Constraint::HorizontalDistance(Distance {
12196 segments: vec![point0_id.into(), point1_id.into()],
12197 distance: Number {
12198 value: 2.0,
12199 units: NumericSuffix::Mm,
12200 },
12201 label_position: Some(label_position.clone()),
12202 source: Default::default(),
12203 });
12204 let (src_delta, scene_delta) = frontend
12205 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12206 .await
12207 .unwrap();
12208 insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12209 assert_eq!(
12210 scene_delta.new_graph.objects.len(),
12211 5,
12212 "{:#?}",
12213 scene_delta.new_graph.objects
12214 );
12215 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12216 let sketch = expect_sketch(sketch_object);
12217 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12218 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12219 panic!("Expected constraint object");
12220 };
12221 let Constraint::HorizontalDistance(distance) = constraint else {
12222 panic!("Expected horizontal distance constraint");
12223 };
12224 assert_eq!(distance.label_position, Some(label_position));
12225
12226 mock_ctx.close().await;
12227 }
12228
12229 #[tokio::test(flavor = "multi_thread")]
12230 async fn test_radius_single_arc_segment() {
12231 let initial_source = "\
12232sketch(on = XY) {
12233 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12234}
12235";
12236
12237 let program = Program::parse(initial_source).unwrap().0.unwrap();
12238
12239 let mut frontend = FrontendState::new();
12240
12241 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12242 let mock_ctx = ExecutorContext::new_mock(None).await;
12243 let version = Version(0);
12244
12245 frontend.hack_set_program(&ctx, program).await.unwrap();
12246 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12247 let sketch_id = sketch_object.id;
12248 let sketch = expect_sketch(sketch_object);
12249 let arc_id = sketch
12251 .segments
12252 .iter()
12253 .find(|&seg_id| {
12254 let obj = frontend.scene_graph.objects.get(seg_id.0);
12255 matches!(
12256 obj.map(|o| &o.kind),
12257 Some(ObjectKind::Segment {
12258 segment: Segment::Arc(_)
12259 })
12260 )
12261 })
12262 .unwrap();
12263
12264 let constraint = Constraint::Radius(Radius {
12265 arc: *arc_id,
12266 radius: Number {
12267 value: 5.0,
12268 units: NumericSuffix::Mm,
12269 },
12270 label_position: None,
12271 source: Default::default(),
12272 });
12273 let (src_delta, scene_delta) = frontend
12274 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12275 .await
12276 .unwrap();
12277 insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12278 assert_eq!(
12279 scene_delta.new_graph.objects.len(),
12280 7, "{:#?}",
12282 scene_delta.new_graph.objects
12283 );
12284
12285 ctx.close().await;
12286 mock_ctx.close().await;
12287 }
12288
12289 #[tokio::test(flavor = "multi_thread")]
12290 async fn test_radius_single_arc_segment_with_label_position() {
12291 let initial_source = "\
12292sketch(on = XY) {
12293 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12294}
12295";
12296
12297 let program = Program::parse(initial_source).unwrap().0.unwrap();
12298 let mut frontend = FrontendState::new();
12299 let mock_ctx = ExecutorContext::new_mock(None).await;
12300 let version = Version(0);
12301
12302 frontend.program = program.clone();
12303 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12304 frontend.update_state_after_exec(outcome, true);
12305 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12306 let sketch_id = sketch_object.id;
12307 let sketch = expect_sketch(sketch_object);
12308 let arc_id = sketch
12309 .segments
12310 .iter()
12311 .find(|&seg_id| {
12312 let obj = frontend.scene_graph.objects.get(seg_id.0);
12313 matches!(
12314 obj.map(|o| &o.kind),
12315 Some(ObjectKind::Segment {
12316 segment: Segment::Arc(_)
12317 })
12318 )
12319 })
12320 .unwrap();
12321
12322 let label_position = Point2d {
12323 x: Number {
12324 value: 10.0,
12325 units: NumericSuffix::Mm,
12326 },
12327 y: Number {
12328 value: 11.0,
12329 units: NumericSuffix::Mm,
12330 },
12331 };
12332 let constraint = Constraint::Radius(Radius {
12333 arc: *arc_id,
12334 radius: Number {
12335 value: 5.0,
12336 units: NumericSuffix::Mm,
12337 },
12338 label_position: Some(label_position.clone()),
12339 source: Default::default(),
12340 });
12341 let (src_delta, scene_delta) = frontend
12342 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12343 .await
12344 .unwrap();
12345 insta::assert_snapshot!(
12346 "test_radius_single_arc_segment_with_label_position",
12347 src_delta.text.as_str()
12348 );
12349
12350 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12351 let sketch = expect_sketch(sketch_object);
12352 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12353 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12354 panic!("Expected constraint object");
12355 };
12356 let Constraint::Radius(radius) = constraint else {
12357 panic!("Expected radius constraint");
12358 };
12359 assert_eq!(radius.label_position, Some(label_position));
12360
12361 mock_ctx.close().await;
12362 }
12363
12364 #[tokio::test(flavor = "multi_thread")]
12365 async fn test_edit_radius_constraint_label_position() {
12366 let initial_source = "\
12367sketch(on = XY) {
12368 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12369 radius(arc1) == 5mm
12370}
12371";
12372
12373 let program = Program::parse(initial_source).unwrap().0.unwrap();
12374 let mut frontend = FrontendState::new();
12375 let mock_ctx = ExecutorContext::new_mock(None).await;
12376 let version = Version(0);
12377
12378 frontend.program = program.clone();
12379 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12380 frontend.update_state_after_exec(outcome, true);
12381 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12382 let sketch_id = sketch_object.id;
12383 let sketch = expect_sketch(sketch_object);
12384 let constraint_id = sketch.constraints[0];
12385 let label_position = Point2d {
12386 x: Number {
12387 value: 10.0,
12388 units: NumericSuffix::Mm,
12389 },
12390 y: Number {
12391 value: 11.0,
12392 units: NumericSuffix::Mm,
12393 },
12394 };
12395
12396 let (src_delta, scene_delta) = frontend
12397 .edit_distance_constraint_label_position(
12398 &mock_ctx,
12399 version,
12400 sketch_id,
12401 constraint_id,
12402 label_position.clone(),
12403 vec![],
12404 )
12405 .await
12406 .unwrap();
12407 insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12408
12409 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12410 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12411 panic!("Expected constraint object");
12412 };
12413 let Constraint::Radius(radius) = constraint else {
12414 panic!("Expected radius constraint");
12415 };
12416 assert_eq!(radius.label_position, Some(label_position));
12417
12418 mock_ctx.close().await;
12419 }
12420
12421 #[tokio::test(flavor = "multi_thread")]
12422 async fn test_vertical_distance_two_points() {
12423 let initial_source = "\
12424sketch(on = XY) {
12425 point(at = [var 1, var 2])
12426 point(at = [var 3, var 4])
12427}
12428";
12429
12430 let program = Program::parse(initial_source).unwrap().0.unwrap();
12431
12432 let mut frontend = FrontendState::new();
12433
12434 let mock_ctx = ExecutorContext::new_mock(None).await;
12435 let version = Version(0);
12436
12437 frontend.program = program.clone();
12438 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12439 frontend.update_state_after_exec(outcome, true);
12440 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12441 let sketch_id = sketch_object.id;
12442 let sketch = expect_sketch(sketch_object);
12443 let point0_id = *sketch.segments.first().unwrap();
12444 let point1_id = *sketch.segments.get(1).unwrap();
12445 let label_position = Point2d {
12446 x: Number {
12447 value: 10.0,
12448 units: NumericSuffix::Mm,
12449 },
12450 y: Number {
12451 value: 11.0,
12452 units: NumericSuffix::Mm,
12453 },
12454 };
12455
12456 let constraint = Constraint::VerticalDistance(Distance {
12457 segments: vec![point0_id.into(), point1_id.into()],
12458 distance: Number {
12459 value: 2.0,
12460 units: NumericSuffix::Mm,
12461 },
12462 label_position: Some(label_position.clone()),
12463 source: Default::default(),
12464 });
12465 let (src_delta, scene_delta) = frontend
12466 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12467 .await
12468 .unwrap();
12469 insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12470 assert_eq!(
12471 scene_delta.new_graph.objects.len(),
12472 5,
12473 "{:#?}",
12474 scene_delta.new_graph.objects
12475 );
12476 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12477 let sketch = expect_sketch(sketch_object);
12478 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12479 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12480 panic!("Expected constraint object");
12481 };
12482 let Constraint::VerticalDistance(distance) = constraint else {
12483 panic!("Expected vertical distance constraint");
12484 };
12485 assert_eq!(distance.label_position, Some(label_position));
12486
12487 mock_ctx.close().await;
12488 }
12489
12490 #[tokio::test(flavor = "multi_thread")]
12491 async fn test_add_fixed_standalone_point() {
12492 let initial_source = "\
12493sketch(on = XY) {
12494 point(at = [var 1, var 2])
12495}
12496";
12497
12498 let program = Program::parse(initial_source).unwrap().0.unwrap();
12499
12500 let mut frontend = FrontendState::new();
12501
12502 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12503 let mock_ctx = ExecutorContext::new_mock(None).await;
12504 let version = Version(0);
12505
12506 frontend.hack_set_program(&ctx, program).await.unwrap();
12507 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12508 let sketch_id = sketch_object.id;
12509 let sketch = expect_sketch(sketch_object);
12510 let point_id = *sketch.segments.first().unwrap();
12511
12512 let (src_delta, scene_delta) = frontend
12513 .add_constraint(
12514 &mock_ctx,
12515 version,
12516 sketch_id,
12517 Constraint::Fixed(Fixed {
12518 points: vec![FixedPoint {
12519 point: point_id,
12520 position: Point2d {
12521 x: Number {
12522 value: 2.0,
12523 units: NumericSuffix::Mm,
12524 },
12525 y: Number {
12526 value: 3.0,
12527 units: NumericSuffix::Mm,
12528 },
12529 },
12530 }],
12531 }),
12532 )
12533 .await
12534 .unwrap();
12535 insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12536 assert_eq!(
12537 scene_delta.new_graph.objects.len(),
12538 4,
12539 "{:#?}",
12540 scene_delta.new_graph.objects
12541 );
12542
12543 ctx.close().await;
12544 mock_ctx.close().await;
12545 }
12546
12547 #[tokio::test(flavor = "multi_thread")]
12548 async fn test_add_fixed_multiple_points() {
12549 let initial_source = "\
12550sketch(on = XY) {
12551 point(at = [var 1, var 2])
12552 point(at = [var 3, var 4])
12553}
12554";
12555
12556 let program = Program::parse(initial_source).unwrap().0.unwrap();
12557
12558 let mut frontend = FrontendState::new();
12559
12560 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12561 let mock_ctx = ExecutorContext::new_mock(None).await;
12562 let version = Version(0);
12563
12564 frontend.hack_set_program(&ctx, program).await.unwrap();
12565 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12566 let sketch_id = sketch_object.id;
12567 let sketch = expect_sketch(sketch_object);
12568 let point0_id = *sketch.segments.first().unwrap();
12569 let point1_id = *sketch.segments.get(1).unwrap();
12570
12571 let (src_delta, scene_delta) = frontend
12572 .add_constraint(
12573 &mock_ctx,
12574 version,
12575 sketch_id,
12576 Constraint::Fixed(Fixed {
12577 points: vec![
12578 FixedPoint {
12579 point: point0_id,
12580 position: Point2d {
12581 x: Number {
12582 value: 2.0,
12583 units: NumericSuffix::Mm,
12584 },
12585 y: Number {
12586 value: 3.0,
12587 units: NumericSuffix::Mm,
12588 },
12589 },
12590 },
12591 FixedPoint {
12592 point: point1_id,
12593 position: Point2d {
12594 x: Number {
12595 value: 4.0,
12596 units: NumericSuffix::Mm,
12597 },
12598 y: Number {
12599 value: 5.0,
12600 units: NumericSuffix::Mm,
12601 },
12602 },
12603 },
12604 ],
12605 }),
12606 )
12607 .await
12608 .unwrap();
12609 insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12610 assert_eq!(
12611 scene_delta.new_graph.objects.len(),
12612 6,
12613 "{:#?}",
12614 scene_delta.new_graph.objects
12615 );
12616
12617 ctx.close().await;
12618 mock_ctx.close().await;
12619 }
12620
12621 #[tokio::test(flavor = "multi_thread")]
12622 async fn test_add_fixed_owned_point() {
12623 let initial_source = "\
12624sketch(on = XY) {
12625 line(start = [var 1, var 2], end = [var 3, var 4])
12626}
12627";
12628
12629 let program = Program::parse(initial_source).unwrap().0.unwrap();
12630
12631 let mut frontend = FrontendState::new();
12632
12633 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12634 let mock_ctx = ExecutorContext::new_mock(None).await;
12635 let version = Version(0);
12636
12637 frontend.hack_set_program(&ctx, program).await.unwrap();
12638 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12639 let sketch_id = sketch_object.id;
12640 let sketch = expect_sketch(sketch_object);
12641 let line_start_id = *sketch.segments.first().unwrap();
12642
12643 let (src_delta, scene_delta) = frontend
12644 .add_constraint(
12645 &mock_ctx,
12646 version,
12647 sketch_id,
12648 Constraint::Fixed(Fixed {
12649 points: vec![FixedPoint {
12650 point: line_start_id,
12651 position: Point2d {
12652 x: Number {
12653 value: 2.0,
12654 units: NumericSuffix::Mm,
12655 },
12656 y: Number {
12657 value: 3.0,
12658 units: NumericSuffix::Mm,
12659 },
12660 },
12661 }],
12662 }),
12663 )
12664 .await
12665 .unwrap();
12666 insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12667 assert_eq!(
12668 scene_delta.new_graph.objects.len(),
12669 6,
12670 "{:#?}",
12671 scene_delta.new_graph.objects
12672 );
12673
12674 ctx.close().await;
12675 mock_ctx.close().await;
12676 }
12677
12678 #[tokio::test(flavor = "multi_thread")]
12679 async fn test_radius_error_cases() {
12680 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12681 let mock_ctx = ExecutorContext::new_mock(None).await;
12682 let version = Version(0);
12683
12684 let initial_source_point = "\
12686sketch(on = XY) {
12687 point(at = [var 1, var 2])
12688}
12689";
12690 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12691 let mut frontend_point = FrontendState::new();
12692 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12693 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12694 let sketch_id_point = sketch_object_point.id;
12695 let sketch_point = expect_sketch(sketch_object_point);
12696 let point_id = *sketch_point.segments.first().unwrap();
12697
12698 let constraint_point = Constraint::Radius(Radius {
12699 arc: point_id,
12700 radius: Number {
12701 value: 5.0,
12702 units: NumericSuffix::Mm,
12703 },
12704 label_position: None,
12705 source: Default::default(),
12706 });
12707 let result_point = frontend_point
12708 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12709 .await;
12710 assert!(result_point.is_err(), "Single point should error for radius");
12711
12712 let initial_source_line = "\
12714sketch(on = XY) {
12715 line(start = [var 1, var 2], end = [var 3, var 4])
12716}
12717";
12718 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12719 let mut frontend_line = FrontendState::new();
12720 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12721 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12722 let sketch_id_line = sketch_object_line.id;
12723 let sketch_line = expect_sketch(sketch_object_line);
12724 let line_id = *sketch_line.segments.first().unwrap();
12725
12726 let constraint_line = Constraint::Radius(Radius {
12727 arc: line_id,
12728 radius: Number {
12729 value: 5.0,
12730 units: NumericSuffix::Mm,
12731 },
12732 label_position: None,
12733 source: Default::default(),
12734 });
12735 let result_line = frontend_line
12736 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12737 .await;
12738 assert!(result_line.is_err(), "Single line segment should error for radius");
12739
12740 ctx.close().await;
12741 mock_ctx.close().await;
12742 }
12743
12744 #[tokio::test(flavor = "multi_thread")]
12745 async fn test_diameter_single_arc_segment() {
12746 let initial_source = "\
12747sketch(on = XY) {
12748 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12749}
12750";
12751
12752 let program = Program::parse(initial_source).unwrap().0.unwrap();
12753
12754 let mut frontend = FrontendState::new();
12755
12756 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12757 let mock_ctx = ExecutorContext::new_mock(None).await;
12758 let version = Version(0);
12759
12760 frontend.hack_set_program(&ctx, program).await.unwrap();
12761 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12762 let sketch_id = sketch_object.id;
12763 let sketch = expect_sketch(sketch_object);
12764 let arc_id = sketch
12766 .segments
12767 .iter()
12768 .find(|&seg_id| {
12769 let obj = frontend.scene_graph.objects.get(seg_id.0);
12770 matches!(
12771 obj.map(|o| &o.kind),
12772 Some(ObjectKind::Segment {
12773 segment: Segment::Arc(_)
12774 })
12775 )
12776 })
12777 .unwrap();
12778
12779 let constraint = Constraint::Diameter(Diameter {
12780 arc: *arc_id,
12781 diameter: Number {
12782 value: 10.0,
12783 units: NumericSuffix::Mm,
12784 },
12785 label_position: None,
12786 source: Default::default(),
12787 });
12788 let (src_delta, scene_delta) = frontend
12789 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12790 .await
12791 .unwrap();
12792 insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12793 assert_eq!(
12794 scene_delta.new_graph.objects.len(),
12795 7, "{:#?}",
12797 scene_delta.new_graph.objects
12798 );
12799
12800 ctx.close().await;
12801 mock_ctx.close().await;
12802 }
12803
12804 #[tokio::test(flavor = "multi_thread")]
12805 async fn test_diameter_single_arc_segment_with_label_position() {
12806 let initial_source = "\
12807sketch(on = XY) {
12808 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12809}
12810";
12811
12812 let program = Program::parse(initial_source).unwrap().0.unwrap();
12813 let mut frontend = FrontendState::new();
12814 let mock_ctx = ExecutorContext::new_mock(None).await;
12815 let version = Version(0);
12816
12817 frontend.program = program.clone();
12818 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12819 frontend.update_state_after_exec(outcome, true);
12820 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12821 let sketch_id = sketch_object.id;
12822 let sketch = expect_sketch(sketch_object);
12823 let arc_id = sketch
12824 .segments
12825 .iter()
12826 .find(|&seg_id| {
12827 let obj = frontend.scene_graph.objects.get(seg_id.0);
12828 matches!(
12829 obj.map(|o| &o.kind),
12830 Some(ObjectKind::Segment {
12831 segment: Segment::Arc(_)
12832 })
12833 )
12834 })
12835 .unwrap();
12836
12837 let label_position = Point2d {
12838 x: Number {
12839 value: 10.0,
12840 units: NumericSuffix::Mm,
12841 },
12842 y: Number {
12843 value: 11.0,
12844 units: NumericSuffix::Mm,
12845 },
12846 };
12847 let constraint = Constraint::Diameter(Diameter {
12848 arc: *arc_id,
12849 diameter: Number {
12850 value: 10.0,
12851 units: NumericSuffix::Mm,
12852 },
12853 label_position: Some(label_position.clone()),
12854 source: Default::default(),
12855 });
12856 let (src_delta, scene_delta) = frontend
12857 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12858 .await
12859 .unwrap();
12860 insta::assert_snapshot!(
12861 "test_diameter_single_arc_segment_with_label_position",
12862 src_delta.text.as_str()
12863 );
12864
12865 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12866 let sketch = expect_sketch(sketch_object);
12867 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12868 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12869 panic!("Expected constraint object");
12870 };
12871 let Constraint::Diameter(diameter) = constraint else {
12872 panic!("Expected diameter constraint");
12873 };
12874 assert_eq!(diameter.label_position, Some(label_position));
12875
12876 mock_ctx.close().await;
12877 }
12878
12879 #[tokio::test(flavor = "multi_thread")]
12880 async fn test_edit_diameter_constraint_label_position() {
12881 let initial_source = "\
12882sketch(on = XY) {
12883 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12884 diameter(arc1) == 10mm
12885}
12886";
12887
12888 let program = Program::parse(initial_source).unwrap().0.unwrap();
12889 let mut frontend = FrontendState::new();
12890 let mock_ctx = ExecutorContext::new_mock(None).await;
12891 let version = Version(0);
12892
12893 frontend.program = program.clone();
12894 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12895 frontend.update_state_after_exec(outcome, true);
12896 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12897 let sketch_id = sketch_object.id;
12898 let sketch = expect_sketch(sketch_object);
12899 let constraint_id = sketch.constraints[0];
12900 let label_position = Point2d {
12901 x: Number {
12902 value: 10.0,
12903 units: NumericSuffix::Mm,
12904 },
12905 y: Number {
12906 value: 11.0,
12907 units: NumericSuffix::Mm,
12908 },
12909 };
12910
12911 let (src_delta, scene_delta) = frontend
12912 .edit_distance_constraint_label_position(
12913 &mock_ctx,
12914 version,
12915 sketch_id,
12916 constraint_id,
12917 label_position.clone(),
12918 vec![],
12919 )
12920 .await
12921 .unwrap();
12922 insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12923
12924 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12925 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12926 panic!("Expected constraint object");
12927 };
12928 let Constraint::Diameter(diameter) = constraint else {
12929 panic!("Expected diameter constraint");
12930 };
12931 assert_eq!(diameter.label_position, Some(label_position));
12932
12933 mock_ctx.close().await;
12934 }
12935
12936 #[tokio::test(flavor = "multi_thread")]
12937 async fn test_diameter_error_cases() {
12938 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12939 let mock_ctx = ExecutorContext::new_mock(None).await;
12940 let version = Version(0);
12941
12942 let initial_source_point = "\
12944sketch(on = XY) {
12945 point(at = [var 1, var 2])
12946}
12947";
12948 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12949 let mut frontend_point = FrontendState::new();
12950 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12951 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12952 let sketch_id_point = sketch_object_point.id;
12953 let sketch_point = expect_sketch(sketch_object_point);
12954 let point_id = *sketch_point.segments.first().unwrap();
12955
12956 let constraint_point = Constraint::Diameter(Diameter {
12957 arc: point_id,
12958 diameter: Number {
12959 value: 10.0,
12960 units: NumericSuffix::Mm,
12961 },
12962 label_position: None,
12963 source: Default::default(),
12964 });
12965 let result_point = frontend_point
12966 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12967 .await;
12968 assert!(result_point.is_err(), "Single point should error for diameter");
12969
12970 let initial_source_line = "\
12972sketch(on = XY) {
12973 line(start = [var 1, var 2], end = [var 3, var 4])
12974}
12975";
12976 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12977 let mut frontend_line = FrontendState::new();
12978 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12979 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12980 let sketch_id_line = sketch_object_line.id;
12981 let sketch_line = expect_sketch(sketch_object_line);
12982 let line_id = *sketch_line.segments.first().unwrap();
12983
12984 let constraint_line = Constraint::Diameter(Diameter {
12985 arc: line_id,
12986 diameter: Number {
12987 value: 10.0,
12988 units: NumericSuffix::Mm,
12989 },
12990 label_position: None,
12991 source: Default::default(),
12992 });
12993 let result_line = frontend_line
12994 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12995 .await;
12996 assert!(result_line.is_err(), "Single line segment should error for diameter");
12997
12998 ctx.close().await;
12999 mock_ctx.close().await;
13000 }
13001
13002 #[tokio::test(flavor = "multi_thread")]
13003 async fn test_line_horizontal() {
13004 let initial_source = "\
13005sketch(on = XY) {
13006 line(start = [var 1, var 2], end = [var 3, var 4])
13007}
13008";
13009
13010 let program = Program::parse(initial_source).unwrap().0.unwrap();
13011
13012 let mut frontend = FrontendState::new();
13013
13014 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13015 let mock_ctx = ExecutorContext::new_mock(None).await;
13016 let version = Version(0);
13017
13018 frontend.hack_set_program(&ctx, program).await.unwrap();
13019 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13020 let sketch_id = sketch_object.id;
13021 let sketch = expect_sketch(sketch_object);
13022 let line1_id = *sketch.segments.get(2).unwrap();
13023
13024 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13025 let (src_delta, scene_delta) = frontend
13026 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13027 .await
13028 .unwrap();
13029 insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13030 assert_eq!(
13031 scene_delta.new_graph.objects.len(),
13032 6,
13033 "{:#?}",
13034 scene_delta.new_graph.objects
13035 );
13036
13037 ctx.close().await;
13038 mock_ctx.close().await;
13039 }
13040
13041 #[tokio::test(flavor = "multi_thread")]
13042 async fn test_control_point_spline_edge_horizontal() {
13043 let initial_source = "\
13044@settings(experimentalFeatures = allow)
13045splineSketch = sketch(on = XY) {
13046 controlPointSpline1 = controlPointSpline(points = [
13047 [var 0mm, var 0mm],
13048 [var 10mm, var 20mm],
13049 [var 20mm, var 0mm],
13050 ])
13051}
13052";
13053
13054 let program = Program::parse(initial_source).unwrap().0.unwrap();
13055
13056 let mut frontend = FrontendState::new();
13057
13058 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13059 let mock_ctx = ExecutorContext::new_mock(None).await;
13060 let version = Version(0);
13061
13062 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13063 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13064 let sketch_id = sketch_object.id;
13065 let sketch = expect_sketch(sketch_object);
13066 let spline_id = sketch
13067 .segments
13068 .iter()
13069 .copied()
13070 .find(|seg_id| {
13071 matches!(
13072 &frontend.scene_graph.objects[seg_id.0].kind,
13073 ObjectKind::Segment {
13074 segment: Segment::ControlPointSpline(_)
13075 }
13076 )
13077 })
13078 .expect("Expected a control point spline segment in sketch");
13079 let edge_id = frontend
13080 .scene_graph
13081 .objects
13082 .iter()
13083 .find_map(|obj| match &obj.kind {
13084 ObjectKind::Segment {
13085 segment: Segment::Line(line),
13086 } if line.owner == Some(spline_id) => Some(obj.id),
13087 _ => None,
13088 })
13089 .expect("Expected an owned control-polygon edge");
13090
13091 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13092 let (src_delta, _) = frontend
13093 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13094 .await
13095 .unwrap();
13096 assert!(
13097 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13098 "Expected horizontal constraint on spline edge, got: {}",
13099 src_delta.text
13100 );
13101
13102 ctx.close().await;
13103 mock_ctx.close().await;
13104 }
13105
13106 #[tokio::test(flavor = "multi_thread")]
13107 async fn test_control_point_spline_edge_angle() {
13108 let initial_source = "\
13109@settings(experimentalFeatures = allow)
13110splineSketch = sketch(on = XY) {
13111 controlPointSpline1 = controlPointSpline(points = [
13112 [var 0mm, var 0mm],
13113 [var 10mm, var 20mm],
13114 [var 20mm, var 0mm],
13115 ])
13116
13117 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13118}
13119";
13120
13121 let program = Program::parse(initial_source).unwrap().0.unwrap();
13122
13123 let mut frontend = FrontendState::new();
13124
13125 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13126 let mock_ctx = ExecutorContext::new_mock(None).await;
13127 let version = Version(0);
13128
13129 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13130 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13131 let sketch_id = sketch_object.id;
13132 let sketch = expect_sketch(sketch_object);
13133 let spline_id = sketch
13134 .segments
13135 .iter()
13136 .copied()
13137 .find(|seg_id| {
13138 matches!(
13139 &frontend.scene_graph.objects[seg_id.0].kind,
13140 ObjectKind::Segment {
13141 segment: Segment::ControlPointSpline(_)
13142 }
13143 )
13144 })
13145 .expect("Expected a control point spline segment in sketch");
13146 let edge_id = frontend
13147 .scene_graph
13148 .objects
13149 .iter()
13150 .find_map(|obj| match &obj.kind {
13151 ObjectKind::Segment {
13152 segment: Segment::Line(line),
13153 } if line.owner == Some(spline_id) => Some(obj.id),
13154 _ => None,
13155 })
13156 .expect("Expected an owned control-polygon edge");
13157 let line1_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.is_none() && obj.label == "line1" => Some(obj.id),
13165 _ => None,
13166 })
13167 .or_else(|| {
13168 sketch.segments.iter().copied().find(|seg_id| {
13169 matches!(
13170 &frontend.scene_graph.objects[seg_id.0].kind,
13171 ObjectKind::Segment {
13172 segment: Segment::Line(line),
13173 } if line.owner.is_none()
13174 )
13175 })
13176 })
13177 .expect("Expected a standalone line segment in sketch");
13178
13179 let constraint = Constraint::Angle(Angle {
13180 lines: vec![line1_id, edge_id],
13181 angle: Number {
13182 value: 30.0,
13183 units: NumericSuffix::Deg,
13184 },
13185 sector: None,
13186 inverse: None,
13187 label_position: None,
13188 source: Default::default(),
13189 });
13190 let (src_delta, _) = frontend
13191 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13192 .await
13193 .unwrap();
13194 assert!(
13195 src_delta
13196 .text
13197 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13198 "Expected angle constraint on spline edge, got: {}",
13199 src_delta.text
13200 );
13201
13202 ctx.close().await;
13203 mock_ctx.close().await;
13204 }
13205
13206 #[tokio::test(flavor = "multi_thread")]
13207 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13208 let initial_source = "\
13209@settings(experimentalFeatures = allow)
13210splineSketch = sketch(on = XY) {
13211 spline1 = controlPointSpline(points = [
13212 [var 0mm, var 0mm],
13213 [var 10mm, var 20mm],
13214 [var 20mm, var 0mm],
13215 ])
13216 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13217 coincident([spline1.controls[1], spline1.edges[0]])
13218 coincident([spline1.controls[0], line1])
13219}
13220";
13221
13222 let program = Program::parse(initial_source).unwrap().0.unwrap();
13223
13224 let mut frontend = FrontendState::new();
13225
13226 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13227 let mock_ctx = ExecutorContext::new_mock(None).await;
13228
13229 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13230
13231 let ui_scene_graph = frontend.scene_graph_for_ui();
13232 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13233 let sketch = expect_sketch(sketch_object);
13234
13235 assert_eq!(
13236 sketch.constraints.len(),
13237 1,
13238 "Expected only the external coincident constraint to remain visible in the UI scene graph"
13239 );
13240
13241 let visible_constraints = ui_scene_graph
13242 .objects
13243 .iter()
13244 .filter_map(|object| match &object.kind {
13245 ObjectKind::Constraint {
13246 constraint: Constraint::Coincident(coincident),
13247 } => Some(coincident.clone()),
13248 _ => None,
13249 })
13250 .collect::<Vec<_>>();
13251
13252 assert_eq!(
13253 visible_constraints.len(),
13254 1,
13255 "Expected only one coincident constraint object in the UI scene graph"
13256 );
13257 assert_eq!(
13258 visible_constraints[0].get_segments().len(),
13259 2,
13260 "Expected the remaining visible coincident constraint to reference two segments"
13261 );
13262
13263 ctx.close().await;
13264 mock_ctx.close().await;
13265 }
13266
13267 #[tokio::test(flavor = "multi_thread")]
13268 async fn test_edit_control_point_spline_can_append_control_point() {
13269 let initial_source = "\
13270@settings(experimentalFeatures = allow)
13271splineSketch = sketch(on = XY) {
13272 controlPointSpline(points = [
13273 [var 0mm, var 0mm],
13274 [var 10mm, var 20mm],
13275 [var 20mm, var 0mm],
13276 ])
13277}
13278";
13279
13280 let program = Program::parse(initial_source).unwrap().0.unwrap();
13281
13282 let mut frontend = FrontendState::new();
13283
13284 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13285 let mock_ctx = ExecutorContext::new_mock(None).await;
13286 let version = Version(0);
13287
13288 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13289 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13290 let sketch_id = sketch_object.id;
13291 let sketch = expect_sketch(sketch_object);
13292 let spline_id = sketch
13293 .segments
13294 .iter()
13295 .copied()
13296 .find(|seg_id| {
13297 matches!(
13298 &frontend.scene_graph.objects[seg_id.0].kind,
13299 ObjectKind::Segment {
13300 segment: Segment::ControlPointSpline(_)
13301 }
13302 )
13303 })
13304 .expect("Expected a control point spline segment in sketch");
13305
13306 let ctor = ControlPointSplineCtor {
13307 points: vec![
13308 Point2d {
13309 x: Expr::Var(Number {
13310 value: 0.0,
13311 units: NumericSuffix::Mm,
13312 }),
13313 y: Expr::Var(Number {
13314 value: 0.0,
13315 units: NumericSuffix::Mm,
13316 }),
13317 },
13318 Point2d {
13319 x: Expr::Var(Number {
13320 value: 10.0,
13321 units: NumericSuffix::Mm,
13322 }),
13323 y: Expr::Var(Number {
13324 value: 20.0,
13325 units: NumericSuffix::Mm,
13326 }),
13327 },
13328 Point2d {
13329 x: Expr::Var(Number {
13330 value: 20.0,
13331 units: NumericSuffix::Mm,
13332 }),
13333 y: Expr::Var(Number {
13334 value: 0.0,
13335 units: NumericSuffix::Mm,
13336 }),
13337 },
13338 Point2d {
13339 x: Expr::Var(Number {
13340 value: 30.0,
13341 units: NumericSuffix::Mm,
13342 }),
13343 y: Expr::Var(Number {
13344 value: 10.0,
13345 units: NumericSuffix::Mm,
13346 }),
13347 },
13348 ],
13349 construction: None,
13350 };
13351
13352 let segments = vec![ExistingSegmentCtor {
13353 id: spline_id,
13354 ctor: SegmentCtor::ControlPointSpline(ctor),
13355 }];
13356 let (src_delta, scene_delta) = frontend
13357 .edit_segments(&mock_ctx, version, sketch_id, segments)
13358 .await
13359 .unwrap();
13360
13361 assert!(
13362 src_delta.text.contains("[var 30mm, var 10mm]"),
13363 "Expected appended spline control point in source, got: {}",
13364 src_delta.text
13365 );
13366
13367 assert!(
13368 scene_delta.invalidates_ids,
13369 "Expected appending a spline control point to invalidate ids"
13370 );
13371 let updated_spline = scene_delta
13372 .new_graph
13373 .objects
13374 .iter()
13375 .find_map(|obj| match &obj.kind {
13376 ObjectKind::Segment {
13377 segment: Segment::ControlPointSpline(updated_spline),
13378 } if updated_spline.controls.len() == 4 => Some(updated_spline),
13379 _ => None,
13380 })
13381 .expect("Expected edited scene graph to contain a four-point control point spline");
13382 assert_eq!(
13383 updated_spline.controls.len(),
13384 4,
13385 "Expected edited spline to expose four control points"
13386 );
13387
13388 ctx.close().await;
13389 mock_ctx.close().await;
13390 }
13391
13392 #[tokio::test(flavor = "multi_thread")]
13393 async fn test_line_vertical() {
13394 let initial_source = "\
13395sketch(on = XY) {
13396 line(start = [var 1, var 2], end = [var 3, var 4])
13397}
13398";
13399
13400 let program = Program::parse(initial_source).unwrap().0.unwrap();
13401
13402 let mut frontend = FrontendState::new();
13403
13404 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13405 let mock_ctx = ExecutorContext::new_mock(None).await;
13406 let version = Version(0);
13407
13408 frontend.hack_set_program(&ctx, program).await.unwrap();
13409 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13410 let sketch_id = sketch_object.id;
13411 let sketch = expect_sketch(sketch_object);
13412 let line1_id = *sketch.segments.get(2).unwrap();
13413
13414 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13415 let (src_delta, scene_delta) = frontend
13416 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13417 .await
13418 .unwrap();
13419 insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13420 assert_eq!(
13421 scene_delta.new_graph.objects.len(),
13422 6,
13423 "{:#?}",
13424 scene_delta.new_graph.objects
13425 );
13426
13427 ctx.close().await;
13428 mock_ctx.close().await;
13429 }
13430
13431 #[tokio::test(flavor = "multi_thread")]
13432 async fn test_points_vertical() {
13433 let initial_source = "\
13434sketch001 = sketch(on = XY) {
13435 p0 = point(at = [var -2.23mm, var 3.1mm])
13436 pf = point(at = [4, 4])
13437}
13438";
13439
13440 let program = Program::parse(initial_source).unwrap().0.unwrap();
13441
13442 let mut frontend = FrontendState::new();
13443
13444 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13445 let mock_ctx = ExecutorContext::new_mock(None).await;
13446 let version = Version(0);
13447
13448 frontend.hack_set_program(&ctx, program).await.unwrap();
13449 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13450 let sketch_id = sketch_object.id;
13451 let sketch = expect_sketch(sketch_object);
13452 let point_ids = vec![
13453 sketch.segments.first().unwrap().to_owned(),
13454 sketch.segments.get(1).unwrap().to_owned(),
13455 ];
13456
13457 let constraint = Constraint::Vertical(Vertical::Points {
13458 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13459 });
13460 let (src_delta, scene_delta) = frontend
13461 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13462 .await
13463 .unwrap();
13464 insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13465 assert_eq!(
13466 scene_delta.new_graph.objects.len(),
13467 5,
13468 "{:#?}",
13469 scene_delta.new_graph.objects
13470 );
13471
13472 ctx.close().await;
13473 mock_ctx.close().await;
13474 }
13475
13476 #[tokio::test(flavor = "multi_thread")]
13477 async fn test_points_horizontal() {
13478 let initial_source = "\
13479sketch001 = sketch(on = XY) {
13480 p0 = point(at = [var -2.23mm, var 3.1mm])
13481 pf = point(at = [4, 4])
13482}
13483";
13484
13485 let program = Program::parse(initial_source).unwrap().0.unwrap();
13486
13487 let mut frontend = FrontendState::new();
13488
13489 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13490 let mock_ctx = ExecutorContext::new_mock(None).await;
13491 let version = Version(0);
13492
13493 frontend.hack_set_program(&ctx, program).await.unwrap();
13494 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13495 let sketch_id = sketch_object.id;
13496 let sketch = expect_sketch(sketch_object);
13497 let point_ids = vec![
13498 sketch.segments.first().unwrap().to_owned(),
13499 sketch.segments.get(1).unwrap().to_owned(),
13500 ];
13501
13502 let constraint = Constraint::Horizontal(Horizontal::Points {
13503 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13504 });
13505 let (src_delta, scene_delta) = frontend
13506 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13507 .await
13508 .unwrap();
13509 insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13510 assert_eq!(
13511 scene_delta.new_graph.objects.len(),
13512 5,
13513 "{:#?}",
13514 scene_delta.new_graph.objects
13515 );
13516
13517 ctx.close().await;
13518 mock_ctx.close().await;
13519 }
13520
13521 #[tokio::test(flavor = "multi_thread")]
13522 async fn test_point_horizontal_with_origin() {
13523 let initial_source = "\
13524sketch001 = sketch(on = XY) {
13525 p0 = point(at = [var -2.23mm, var 3.1mm])
13526}
13527";
13528
13529 let program = Program::parse(initial_source).unwrap().0.unwrap();
13530
13531 let mut frontend = FrontendState::new();
13532
13533 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13534 let mock_ctx = ExecutorContext::new_mock(None).await;
13535 let version = Version(0);
13536
13537 frontend.hack_set_program(&ctx, program).await.unwrap();
13538 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13539 let sketch_id = sketch_object.id;
13540 let sketch = expect_sketch(sketch_object);
13541 let point_id = *sketch.segments.first().unwrap();
13542
13543 let constraint = Constraint::Horizontal(Horizontal::Points {
13544 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13545 });
13546 let (src_delta, scene_delta) = frontend
13547 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13548 .await
13549 .unwrap();
13550 insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13551 assert_eq!(
13552 scene_delta.new_graph.objects.len(),
13553 4,
13554 "{:#?}",
13555 scene_delta.new_graph.objects
13556 );
13557
13558 ctx.close().await;
13559 mock_ctx.close().await;
13560 }
13561
13562 #[tokio::test(flavor = "multi_thread")]
13563 async fn test_lines_equal_length() {
13564 let initial_source = "\
13565sketch(on = XY) {
13566 line(start = [var 1, var 2], end = [var 3, var 4])
13567 line(start = [var 5, var 6], end = [var 7, var 8])
13568}
13569";
13570
13571 let program = Program::parse(initial_source).unwrap().0.unwrap();
13572
13573 let mut frontend = FrontendState::new();
13574
13575 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13576 let mock_ctx = ExecutorContext::new_mock(None).await;
13577 let version = Version(0);
13578
13579 frontend.hack_set_program(&ctx, program).await.unwrap();
13580 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13581 let sketch_id = sketch_object.id;
13582 let sketch = expect_sketch(sketch_object);
13583 let line1_id = *sketch.segments.get(2).unwrap();
13584 let line2_id = *sketch.segments.get(5).unwrap();
13585
13586 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13587 lines: vec![line1_id, line2_id],
13588 });
13589 let (src_delta, scene_delta) = frontend
13590 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13591 .await
13592 .unwrap();
13593 insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13594 assert_eq!(
13595 scene_delta.new_graph.objects.len(),
13596 9,
13597 "{:#?}",
13598 scene_delta.new_graph.objects
13599 );
13600
13601 ctx.close().await;
13602 mock_ctx.close().await;
13603 }
13604
13605 #[tokio::test(flavor = "multi_thread")]
13606 async fn test_add_constraint_multi_line_equal_length() {
13607 let initial_source = "\
13608sketch(on = XY) {
13609 line(start = [var 1, var 2], end = [var 3, var 4])
13610 line(start = [var 5, var 6], end = [var 7, var 8])
13611 line(start = [var 9, var 10], end = [var 11, var 12])
13612}
13613";
13614
13615 let program = Program::parse(initial_source).unwrap().0.unwrap();
13616
13617 let mut frontend = FrontendState::new();
13618 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13619 let mock_ctx = ExecutorContext::new_mock(None).await;
13620 let version = Version(0);
13621
13622 frontend.hack_set_program(&ctx, program).await.unwrap();
13623 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13624 let sketch_id = sketch_object.id;
13625 let sketch = expect_sketch(sketch_object);
13626 let line1_id = *sketch.segments.get(2).unwrap();
13627 let line2_id = *sketch.segments.get(5).unwrap();
13628 let line3_id = *sketch.segments.get(8).unwrap();
13629
13630 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13631 lines: vec![line1_id, line2_id, line3_id],
13632 });
13633 let (src_delta, scene_delta) = frontend
13634 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13635 .await
13636 .unwrap();
13637 insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13638 let constraints = scene_delta
13639 .new_graph
13640 .objects
13641 .iter()
13642 .filter_map(|obj| {
13643 let ObjectKind::Constraint { constraint } = &obj.kind else {
13644 return None;
13645 };
13646 Some(constraint)
13647 })
13648 .collect::<Vec<_>>();
13649
13650 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13651 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13652 panic!("expected equal length constraint, got {:?}", constraints[0]);
13653 };
13654 assert_eq!(lines_equal_length.lines.len(), 3);
13655
13656 ctx.close().await;
13657 mock_ctx.close().await;
13658 }
13659
13660 #[tokio::test(flavor = "multi_thread")]
13661 async fn test_lines_parallel() {
13662 let initial_source = "\
13663sketch(on = XY) {
13664 line(start = [var 1, var 2], end = [var 3, var 4])
13665 line(start = [var 5, var 6], end = [var 7, var 8])
13666}
13667";
13668
13669 let program = Program::parse(initial_source).unwrap().0.unwrap();
13670
13671 let mut frontend = FrontendState::new();
13672
13673 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13674 let mock_ctx = ExecutorContext::new_mock(None).await;
13675 let version = Version(0);
13676
13677 frontend.hack_set_program(&ctx, program).await.unwrap();
13678 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13679 let sketch_id = sketch_object.id;
13680 let sketch = expect_sketch(sketch_object);
13681 let line1_id = *sketch.segments.get(2).unwrap();
13682 let line2_id = *sketch.segments.get(5).unwrap();
13683
13684 let constraint = Constraint::Parallel(Parallel {
13685 lines: vec![line1_id, line2_id],
13686 });
13687 let (src_delta, scene_delta) = frontend
13688 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13689 .await
13690 .unwrap();
13691 insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13692 assert_eq!(
13693 scene_delta.new_graph.objects.len(),
13694 9,
13695 "{:#?}",
13696 scene_delta.new_graph.objects
13697 );
13698
13699 ctx.close().await;
13700 mock_ctx.close().await;
13701 }
13702
13703 #[tokio::test(flavor = "multi_thread")]
13704 async fn test_lines_parallel_multiline() {
13705 let initial_source = "\
13706sketch(on = XY) {
13707 line(start = [var 1, var 2], end = [var 3, var 4])
13708 line(start = [var 5, var 6], end = [var 7, var 8])
13709 line(start = [var 9, var 10], end = [var 11, var 12])
13710}
13711";
13712
13713 let program = Program::parse(initial_source).unwrap().0.unwrap();
13714
13715 let mut frontend = FrontendState::new();
13716
13717 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13718 let mock_ctx = ExecutorContext::new_mock(None).await;
13719 let version = Version(0);
13720
13721 frontend.hack_set_program(&ctx, program).await.unwrap();
13722 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13723 let sketch_id = sketch_object.id;
13724 let sketch = expect_sketch(sketch_object);
13725 let line1_id = *sketch.segments.get(2).unwrap();
13726 let line2_id = *sketch.segments.get(5).unwrap();
13727 let line3_id = *sketch.segments.get(8).unwrap();
13728
13729 let constraint = Constraint::Parallel(Parallel {
13730 lines: vec![line1_id, line2_id, line3_id],
13731 });
13732 let (src_delta, scene_delta) = frontend
13733 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13734 .await
13735 .unwrap();
13736 insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13737
13738 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13739 let sketch = expect_sketch(sketch_object);
13740 assert_eq!(sketch.constraints.len(), 1);
13741
13742 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13743 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13744 panic!("Expected constraint object");
13745 };
13746 let Constraint::Parallel(parallel) = constraint else {
13747 panic!("Expected parallel constraint");
13748 };
13749 assert_eq!(parallel.lines.len(), 3);
13750
13751 ctx.close().await;
13752 mock_ctx.close().await;
13753 }
13754
13755 #[tokio::test(flavor = "multi_thread")]
13756 async fn test_lines_perpendicular() {
13757 let initial_source = "\
13758sketch(on = XY) {
13759 line(start = [var 1, var 2], end = [var 3, var 4])
13760 line(start = [var 5, var 6], end = [var 7, var 8])
13761}
13762";
13763
13764 let program = Program::parse(initial_source).unwrap().0.unwrap();
13765
13766 let mut frontend = FrontendState::new();
13767
13768 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13769 let mock_ctx = ExecutorContext::new_mock(None).await;
13770 let version = Version(0);
13771
13772 frontend.hack_set_program(&ctx, program).await.unwrap();
13773 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13774 let sketch_id = sketch_object.id;
13775 let sketch = expect_sketch(sketch_object);
13776 let line1_id = *sketch.segments.get(2).unwrap();
13777 let line2_id = *sketch.segments.get(5).unwrap();
13778
13779 let constraint = Constraint::Perpendicular(Perpendicular {
13780 lines: vec![line1_id, line2_id],
13781 });
13782 let (src_delta, scene_delta) = frontend
13783 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13784 .await
13785 .unwrap();
13786 insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13787 assert_eq!(
13788 scene_delta.new_graph.objects.len(),
13789 9,
13790 "{:#?}",
13791 scene_delta.new_graph.objects
13792 );
13793
13794 ctx.close().await;
13795 mock_ctx.close().await;
13796 }
13797
13798 #[tokio::test(flavor = "multi_thread")]
13799 async fn test_lines_angle() {
13800 let initial_source = "\
13801sketch(on = XY) {
13802 line(start = [var 1, var 2], end = [var 3, var 4])
13803 line(start = [var 5, var 6], end = [var 7, var 8])
13804}
13805";
13806
13807 let program = Program::parse(initial_source).unwrap().0.unwrap();
13808
13809 let mut frontend = FrontendState::new();
13810
13811 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13812 let mock_ctx = ExecutorContext::new_mock(None).await;
13813 let version = Version(0);
13814
13815 frontend.hack_set_program(&ctx, program).await.unwrap();
13816 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13817 let sketch_id = sketch_object.id;
13818 let sketch = expect_sketch(sketch_object);
13819 let line1_id = *sketch.segments.get(2).unwrap();
13820 let line2_id = *sketch.segments.get(5).unwrap();
13821
13822 let constraint = Constraint::Angle(Angle {
13823 lines: vec![line1_id, line2_id],
13824 angle: Number {
13825 value: 30.0,
13826 units: NumericSuffix::Deg,
13827 },
13828 sector: None,
13829 inverse: None,
13830 label_position: None,
13831 source: Default::default(),
13832 });
13833 let (src_delta, scene_delta) = frontend
13834 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13835 .await
13836 .unwrap();
13837 insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13838 assert_eq!(
13839 scene_delta.new_graph.objects.len(),
13840 9,
13841 "{:#?}",
13842 scene_delta.new_graph.objects
13843 );
13844
13845 ctx.close().await;
13846 mock_ctx.close().await;
13847 }
13848
13849 #[tokio::test(flavor = "multi_thread")]
13850 async fn test_lines_angle_with_sector_uses_angle_dimension() {
13851 let initial_source = "\
13852sketch(on = XY) {
13853 line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13854 line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13855}
13856";
13857
13858 let program = Program::parse(initial_source).unwrap().0.unwrap();
13859
13860 let mut frontend = FrontendState::new();
13861
13862 let mock_ctx = ExecutorContext::new_mock(None).await;
13863 let version = Version(0);
13864
13865 frontend.program = program.clone();
13866 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13867 frontend.update_state_after_exec(outcome, true);
13868 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13869 let sketch_id = sketch_object.id;
13870 let sketch = expect_sketch(sketch_object);
13871 let line1_id = *sketch.segments.get(2).unwrap();
13872 let line2_id = *sketch.segments.get(5).unwrap();
13873
13874 let constraint = Constraint::Angle(Angle {
13875 lines: vec![line1_id, line2_id],
13876 angle: Number {
13877 value: 270.0,
13878 units: NumericSuffix::Deg,
13879 },
13880 sector: Some(1),
13881 inverse: Some(true),
13882 label_position: Some(Point2d {
13883 x: Number {
13884 value: -0.73,
13885 units: NumericSuffix::Mm,
13886 },
13887 y: Number {
13888 value: 0.75,
13889 units: NumericSuffix::Mm,
13890 },
13891 }),
13892 source: Default::default(),
13893 });
13894 let (src_delta, _) = frontend
13895 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13896 .await
13897 .unwrap();
13898 assert_eq!(
13899 src_delta.text.as_str(),
13900 "\
13901sketch(on = XY) {
13902 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13903 line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13904 angleDimension(
13905 lines = [line1, line2],
13906 sector = 1,
13907 inverse = true,
13908 labelPosition = [-0.73mm, 0.75mm],
13909) == 270deg
13910}
13911"
13912 );
13913
13914 mock_ctx.close().await;
13915 }
13916
13917 #[tokio::test(flavor = "multi_thread")]
13918 async fn test_segments_tangent() {
13919 let initial_source = "\
13920sketch(on = XY) {
13921 line(start = [var 1, var 2], end = [var 3, var 4])
13922 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13923}
13924";
13925
13926 let program = Program::parse(initial_source).unwrap().0.unwrap();
13927
13928 let mut frontend = FrontendState::new();
13929
13930 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13931 let mock_ctx = ExecutorContext::new_mock(None).await;
13932 let version = Version(0);
13933
13934 frontend.hack_set_program(&ctx, program).await.unwrap();
13935 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13936 let sketch_id = sketch_object.id;
13937 let sketch = expect_sketch(sketch_object);
13938 let line1_id = *sketch.segments.get(2).unwrap();
13939 let arc1_id = *sketch.segments.get(6).unwrap();
13940
13941 let constraint = Constraint::Tangent(Tangent {
13942 input: vec![line1_id, arc1_id],
13943 });
13944 let (src_delta, scene_delta) = frontend
13945 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13946 .await
13947 .unwrap();
13948 insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
13949 assert_eq!(
13950 scene_delta.new_graph.objects.len(),
13951 10,
13952 "{:#?}",
13953 scene_delta.new_graph.objects
13954 );
13955
13956 ctx.close().await;
13957 mock_ctx.close().await;
13958 }
13959
13960 #[tokio::test(flavor = "multi_thread")]
13961 async fn test_point_midpoint() {
13962 let initial_source = "\
13963sketch(on = XY) {
13964 point(at = [var 1, var 1])
13965 line(start = [var 0, var 0], end = [var 6, var 4])
13966}
13967";
13968
13969 let program = Program::parse(initial_source).unwrap().0.unwrap();
13970
13971 let mut frontend = FrontendState::new();
13972
13973 let ctx = ExecutorContext::new_mock(None).await;
13974 let version = Version(0);
13975
13976 frontend.program = program.clone();
13977 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13978 frontend.update_state_after_exec(outcome, true);
13979 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13980 let sketch_id = sketch_object.id;
13981 let sketch = expect_sketch(sketch_object);
13982 let point_id = *sketch.segments.first().unwrap();
13983 let line_id = *sketch.segments.get(3).unwrap();
13984
13985 let constraint = Constraint::Midpoint(Midpoint {
13986 point: ConstraintSegment::from(point_id),
13987 segment: line_id,
13988 });
13989 let (src_delta, scene_delta) = frontend
13990 .add_constraint(&ctx, version, sketch_id, constraint)
13991 .await
13992 .unwrap();
13993 insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
13994 assert_eq!(
13995 scene_delta.new_graph.objects.len(),
13996 7,
13997 "{:#?}",
13998 scene_delta.new_graph.objects
13999 );
14000
14001 ctx.close().await;
14002 }
14003
14004 #[tokio::test(flavor = "multi_thread")]
14005 async fn test_segments_symmetric() {
14006 let initial_source = "\
14007sketch(on = XY) {
14008 line(start = [var 0, var 0], end = [var 0, var 4])
14009 line(start = [var 4, var 0], end = [var 4, var 4])
14010 line(start = [var 2, var -1], end = [var 2, var 5])
14011}
14012";
14013
14014 let program = Program::parse(initial_source).unwrap().0.unwrap();
14015
14016 let mut frontend = FrontendState::new();
14017
14018 let ctx = ExecutorContext::new_mock(None).await;
14019 let version = Version(0);
14020
14021 frontend.program = program.clone();
14022 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14023 frontend.update_state_after_exec(outcome, true);
14024 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14025 let sketch_id = sketch_object.id;
14026 let sketch = expect_sketch(sketch_object);
14027 let line1_id = *sketch.segments.get(2).unwrap();
14028 let line2_id = *sketch.segments.get(5).unwrap();
14029 let axis_id = *sketch.segments.get(8).unwrap();
14030
14031 let constraint = Constraint::Symmetric(Symmetric {
14032 input: vec![line1_id, line2_id],
14033 axis: axis_id,
14034 });
14035 let (src_delta, scene_delta) = frontend
14036 .add_constraint(&ctx, version, sketch_id, constraint)
14037 .await
14038 .unwrap();
14039 insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14040 assert_eq!(
14041 scene_delta.new_graph.objects.len(),
14042 12,
14043 "{:#?}",
14044 scene_delta.new_graph.objects
14045 );
14046
14047 ctx.close().await;
14048 }
14049
14050 #[tokio::test(flavor = "multi_thread")]
14051 async fn test_point_arc_midpoint() {
14052 let initial_source = "\
14053sketch(on = XY) {
14054 point(at = [var 6, var 3])
14055 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14056}
14057";
14058
14059 let program = Program::parse(initial_source).unwrap().0.unwrap();
14060
14061 let mut frontend = FrontendState::new();
14062
14063 let ctx = ExecutorContext::new_mock(None).await;
14064 let version = Version(0);
14065
14066 frontend.program = program.clone();
14067 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14068 frontend.update_state_after_exec(outcome, true);
14069 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14070 let sketch_id = sketch_object.id;
14071 let sketch = expect_sketch(sketch_object);
14072 let point_id = *sketch.segments.first().unwrap();
14073 let arc_id = *sketch.segments.get(4).unwrap();
14074
14075 let constraint = Constraint::Midpoint(Midpoint {
14076 point: ConstraintSegment::from(point_id),
14077 segment: arc_id,
14078 });
14079 let (src_delta, scene_delta) = frontend
14080 .add_constraint(&ctx, version, sketch_id, constraint)
14081 .await
14082 .unwrap();
14083 insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14084 assert_eq!(
14085 scene_delta.new_graph.objects.len(),
14086 8,
14087 "{:#?}",
14088 scene_delta.new_graph.objects
14089 );
14090
14091 ctx.close().await;
14092 }
14093
14094 #[tokio::test(flavor = "multi_thread")]
14095 async fn test_origin_line_midpoint() {
14096 let initial_source = "\
14097sketch(on = XY) {
14098 line(start = [var 0, var 0], end = [var 6, var 4])
14099}
14100";
14101
14102 let program = Program::parse(initial_source).unwrap().0.unwrap();
14103
14104 let mut frontend = FrontendState::new();
14105
14106 let ctx = ExecutorContext::new_mock(None).await;
14107 let version = Version(0);
14108
14109 frontend.program = program.clone();
14110 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14111 frontend.update_state_after_exec(outcome, true);
14112 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14113 let sketch_id = sketch_object.id;
14114 let sketch = expect_sketch(sketch_object);
14115 let line_id = *sketch.segments.get(2).unwrap();
14116
14117 let constraint = Constraint::Midpoint(Midpoint {
14118 point: ConstraintSegment::ORIGIN,
14119 segment: line_id,
14120 });
14121 let (src_delta, scene_delta) = frontend
14122 .add_constraint(&ctx, version, sketch_id, constraint)
14123 .await
14124 .unwrap();
14125 insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14126 assert_eq!(
14127 scene_delta.new_graph.objects.len(),
14128 6,
14129 "{:#?}",
14130 scene_delta.new_graph.objects
14131 );
14132
14133 ctx.close().await;
14134 }
14135
14136 #[tokio::test(flavor = "multi_thread")]
14137 async fn test_origin_arc_midpoint() {
14138 let initial_source = "\
14139sketch(on = XY) {
14140 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14141}
14142";
14143
14144 let program = Program::parse(initial_source).unwrap().0.unwrap();
14145
14146 let mut frontend = FrontendState::new();
14147
14148 let ctx = ExecutorContext::new_mock(None).await;
14149 let version = Version(0);
14150
14151 frontend.program = program.clone();
14152 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14153 frontend.update_state_after_exec(outcome, true);
14154 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14155 let sketch_id = sketch_object.id;
14156 let sketch = expect_sketch(sketch_object);
14157 let arc_id = *sketch.segments.get(3).unwrap();
14158
14159 let constraint = Constraint::Midpoint(Midpoint {
14160 point: ConstraintSegment::ORIGIN,
14161 segment: arc_id,
14162 });
14163 let (src_delta, scene_delta) = frontend
14164 .add_constraint(&ctx, version, sketch_id, constraint)
14165 .await
14166 .unwrap();
14167 insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14168 assert_eq!(
14169 scene_delta.new_graph.objects.len(),
14170 7,
14171 "{:#?}",
14172 scene_delta.new_graph.objects
14173 );
14174
14175 ctx.close().await;
14176 }
14177
14178 #[tokio::test(flavor = "multi_thread")]
14179 async fn test_segments_symmetric_arcs() {
14180 let initial_source = "\
14181sketch(on = XY) {
14182 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14183 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14184 line(start = [var 0, var -10], end = [var 0, var 10])
14185}
14186";
14187
14188 let program = Program::parse(initial_source).unwrap().0.unwrap();
14189
14190 let mut frontend = FrontendState::new();
14191
14192 let ctx = ExecutorContext::new_mock(None).await;
14193 let version = Version(0);
14194
14195 frontend.program = program.clone();
14196 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14197 frontend.update_state_after_exec(outcome, true);
14198 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14199 let sketch_id = sketch_object.id;
14200 let sketch = expect_sketch(sketch_object);
14201 let arc1_id = *sketch.segments.get(3).unwrap();
14202 let arc2_id = *sketch.segments.get(7).unwrap();
14203 let axis_id = *sketch.segments.get(10).unwrap();
14204
14205 let constraint = Constraint::Symmetric(Symmetric {
14206 input: vec![arc1_id, arc2_id],
14207 axis: axis_id,
14208 });
14209 let (src_delta, scene_delta) = frontend
14210 .add_constraint(&ctx, version, sketch_id, constraint)
14211 .await
14212 .unwrap();
14213 insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14214 assert_eq!(
14215 scene_delta.new_graph.objects.len(),
14216 14,
14217 "{:#?}",
14218 scene_delta.new_graph.objects
14219 );
14220
14221 ctx.close().await;
14222 }
14223
14224 #[tokio::test(flavor = "multi_thread")]
14225 async fn test_sketch_on_face_simple() {
14226 let initial_source = "\
14227len = 2mm
14228cube = startSketchOn(XY)
14229 |> startProfile(at = [0, 0])
14230 |> line(end = [len, 0], tag = $side)
14231 |> line(end = [0, len])
14232 |> line(end = [-len, 0])
14233 |> line(end = [0, -len])
14234 |> close()
14235 |> extrude(length = len)
14236
14237face = faceOf(cube, face = side)
14238";
14239
14240 let program = Program::parse(initial_source).unwrap().0.unwrap();
14241
14242 let mut frontend = FrontendState::new();
14243
14244 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14245 let mock_ctx = ExecutorContext::new_mock(None).await;
14246 let version = Version(0);
14247
14248 frontend.hack_set_program(&ctx, program).await.unwrap();
14249 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14250 let face_id = face_object.id;
14251
14252 let sketch_args = SketchCtor {
14253 on: Plane::Object(face_id),
14254 };
14255 let (_src_delta, scene_delta, sketch_id) = frontend
14256 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14257 .await
14258 .unwrap();
14259 assert_eq!(sketch_id, ObjectId(2));
14260 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14261 let sketch_object = &scene_delta.new_graph.objects[2];
14262 assert_eq!(sketch_object.id, ObjectId(2));
14263 assert_eq!(
14264 sketch_object.kind,
14265 ObjectKind::Sketch(Sketch {
14266 args: SketchCtor {
14267 on: Plane::Object(face_id),
14268 },
14269 plane: face_id,
14270 segments: vec![],
14271 constraints: vec![],
14272 })
14273 );
14274 assert_eq!(scene_delta.new_graph.objects.len(), 8);
14275
14276 ctx.close().await;
14277 mock_ctx.close().await;
14278 }
14279
14280 #[tokio::test(flavor = "multi_thread")]
14281 async fn test_new_sketch_on_primitive_index_face() {
14282 let initial_source = "\
14283@settings(kclVersion = 2.0)
14284
14285sketch001 = sketch(on = XY) {
14286 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14287}
14288extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14289shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14290 let program = Program::parse(initial_source).unwrap().0.unwrap();
14291 let ctx = ExecutorContext::new_mock(None).await;
14292 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14293 let solid_id = match outcome.variables.get("shell001") {
14294 Some(KclValueView::Solid { value }) => value.id,
14295 value => panic!("expected shell001 to be a solid, got {value:?}"),
14296 };
14297 let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14298
14299 let mut ast = program.ast;
14300 let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14301 let face_expr = sketch_on_ast_expr(
14302 &mut ast,
14303 &scene_graph,
14304 &solid_references,
14305 &Plane::PrimitiveFace(crate::frontend::api::PrimitiveFacePlane { solid_id, index: 6 }),
14306 )
14307 .unwrap();
14308 let face_decl = ast::VariableDeclaration::new(
14309 ast::VariableDeclarator::new("face001", face_expr),
14310 ast::ItemVisibility::Default,
14311 ast::VariableKind::Const,
14312 );
14313 ast.body
14314 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14315 face_decl,
14316 ))));
14317 let face_source = source_from_ast(&ast);
14318 let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14319 insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14320
14321 let program = Program::parse(&new_source).unwrap().0.unwrap();
14322 ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14323 ctx.close().await;
14324 }
14325
14326 #[tokio::test(flavor = "multi_thread")]
14327 async fn test_sketch_on_wall_artifact_from_region_extrude() {
14328 let initial_source = "\
14329s = sketch(on = YZ) {
14330 line1 = line(start = [0, 0], end = [0, 1])
14331 line2 = line(start = [0, 1], end = [1, 1])
14332 line3 = line(start = [1, 1], end = [0, 0])
14333}
14334region001 = region(point = [0.1, 0.1], sketch = s)
14335extrude001 = extrude(region001, length = 5)
14336";
14337
14338 let program = Program::parse(initial_source).unwrap().0.unwrap();
14339
14340 let mut frontend = FrontendState::new();
14341 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14342 let version = Version(0);
14343
14344 frontend.hack_set_program(&ctx, program).await.unwrap();
14345 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14346
14347 let sketch_args = SketchCtor {
14348 on: Plane::Object(wall_object_id),
14349 };
14350 let (src_delta, _scene_delta, _sketch_id) = frontend
14351 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14352 .await
14353 .unwrap();
14354 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14355
14356 ctx.close().await;
14357 }
14358
14359 #[tokio::test(flavor = "multi_thread")]
14360 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14361 let initial_source = "\
14362sketch001 = sketch(on = YZ) {
14363 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14364 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14365 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14366 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14367 coincident([line1.end, line2.start])
14368 coincident([line2.end, line3.start])
14369 coincident([line3.end, line4.start])
14370 coincident([line4.end, line1.start])
14371 parallel([line2, line4])
14372 parallel([line3, line1])
14373 perpendicular([line1, line2])
14374 horizontal(line3)
14375 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14376}
14377region001 = region(point = [3.1, 3.74], sketch = sketch001)
14378extrude001 = extrude(region001, length = 5)
14379";
14380
14381 let program = Program::parse(initial_source).unwrap().0.unwrap();
14382
14383 let mut frontend = FrontendState::new();
14384 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14385 let version = Version(0);
14386
14387 frontend.hack_set_program(&ctx, program).await.unwrap();
14388 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14389
14390 let sketch_args = SketchCtor {
14391 on: Plane::Object(wall_object_id),
14392 };
14393 let (src_delta, _scene_delta, _sketch_id) = frontend
14394 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14395 .await
14396 .unwrap();
14397 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14398
14399 ctx.close().await;
14400 }
14401
14402 #[tokio::test(flavor = "multi_thread")]
14403 async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14404 let initial_source = "\
14405@settings(kclVersion = 2.0)
14406
14407sketch001 = sketch(on = XY) {
14408 circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14409 circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14410}
14411hidden001 = hide(sketch001)
14412region001 = region(segments = [sketch001.circle2])
14413region002 = region(segments = [sketch001.circle1])
14414extrude001 = extrude([region001, region002], length = 5)
14415";
14416
14417 let program = Program::parse(initial_source).unwrap().0.unwrap();
14418 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14419 let version = Version(0);
14420
14421 for (solid_output_index, expected_face) in [
14422 (0, "faceOf(extrude001[0], face = END)"),
14423 (1, "faceOf(extrude001[1], face = END)"),
14424 ] {
14425 let mut frontend = FrontendState::new();
14426 frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14427 let cap_object_id = find_cap_object_id_with_solid_output_index(
14428 &frontend.scene_graph,
14429 crate::frontend::api::CapKind::End,
14430 solid_output_index,
14431 )
14432 .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14433
14434 let sketch_args = SketchCtor {
14435 on: Plane::Object(cap_object_id),
14436 };
14437 let (src_delta, _scene_delta, _sketch_id) = frontend
14438 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14439 .await
14440 .unwrap();
14441
14442 assert!(
14443 src_delta.text.contains(expected_face),
14444 "expected `{expected_face}` in:\n{}",
14445 src_delta.text
14446 );
14447 assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14448 }
14449
14450 ctx.close().await;
14451 }
14452
14453 #[tokio::test(flavor = "multi_thread")]
14454 async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14455 let initial_source = "\
14456@settings(kclVersion = 2.0)
14457
14458sketch001 = sketch(on = XY) {
14459 rect1Line1 = line(start = [0, 0], end = [1, 0])
14460 rect1Line2 = line(start = [1, 0], end = [1, 1])
14461 rect1Line3 = line(start = [1, 1], end = [0, 1])
14462 rect1Line4 = line(start = [0, 1], end = [0, 0])
14463 rect2Line1 = line(start = [3, 0], end = [4, 0])
14464 rect2Line2 = line(start = [4, 0], end = [4, 1])
14465 rect2Line3 = line(start = [4, 1], end = [3, 1])
14466 rect2Line4 = line(start = [3, 1], end = [3, 0])
14467}
14468hidden001 = hide(sketch001)
14469region001 = region(segments = [
14470 sketch001.rect1Line4,
14471 sketch001.rect1Line1
14472])
14473region002 = region(segments = [
14474 sketch001.rect2Line4,
14475 sketch001.rect2Line1
14476])
14477extrude001 = extrude([region001, region002], length = 5)
14478";
14479
14480 let program = Program::parse(initial_source).unwrap().0.unwrap();
14481 let mut frontend = FrontendState::new();
14482 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14483 let version = Version(0);
14484
14485 frontend.hack_set_program(&ctx, program).await.unwrap();
14486 let region_call = "\
14487region(segments = [
14488 sketch001.rect1Line4,
14489 sketch001.rect1Line1
14490])";
14491 let region_call_start = initial_source.find(region_call).unwrap();
14492 let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14493 let segment_call = "line(start = [0, 0], end = [1, 0])";
14494 let segment_call_start = initial_source.find(segment_call).unwrap();
14495 let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14496 let wall_object_id = frontend
14497 .scene_graph
14498 .objects
14499 .iter()
14500 .find_map(|object| match &object.kind {
14501 ObjectKind::Wall(wall)
14502 if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14503 && wall.source.segment.range == segment_range =>
14504 {
14505 Some(object.id)
14506 }
14507 _ => None,
14508 })
14509 .expect("expected a wall object for region001.tags.rect1Line1");
14510
14511 let sketch_args = SketchCtor {
14512 on: Plane::Object(wall_object_id),
14513 };
14514 let (src_delta, _scene_delta, _sketch_id) = frontend
14515 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14516 .await
14517 .unwrap();
14518
14519 let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14520 assert!(
14521 src_delta.text.contains(expected_face),
14522 "expected `{expected_face}` in:\n{}",
14523 src_delta.text
14524 );
14525 assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14526
14527 ctx.close().await;
14528 }
14529
14530 #[test]
14531 fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14532 let source = "\
14533sketch001 = sketch(on = XY) {
14534 line(start = [0, 0], end = [1, 0])
14535}
14536part = subtract(boxSolid, tools = [cutSolid])
14537 |> appearance(color = \"#8f96a3\")
14538";
14539 let ast = Program::parse(source).unwrap().0.unwrap().ast;
14540 let line_start = source.find("line").unwrap();
14541 let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14542 let line_ref = SourceRef::Simple {
14543 range: [line_start, line_end, 0].into(),
14544 node_path: None,
14545 };
14546 assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14547
14548 let subtract_start = source.find("subtract").unwrap();
14549 let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14550 let subtract_ref = SourceRef::Simple {
14551 range: [subtract_start, subtract_end, 0].into(),
14552 node_path: None,
14553 };
14554 assert_eq!(
14555 variable_name_containing_source_ref(&ast, &subtract_ref),
14556 Some("part".to_owned())
14557 );
14558 }
14559
14560 #[tokio::test(flavor = "multi_thread")]
14561 async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14562 clear_mem_cache().await;
14563 let source = "\
14564boxSolid = startSketchOn(XY)
14565 |> startProfile(at = [0, 0])
14566 |> line(end = [4, 0], tag = $bottomEdge)
14567 |> line(end = [0, 4])
14568 |> line(end = [-4, 0])
14569 |> close()
14570 |> extrude(length = 10)
14571cutSolid = startSketchOn(XY)
14572 |> startProfile(at = [1, 1])
14573 |> line(end = [1, 0])
14574 |> line(end = [0, 1])
14575 |> line(end = [-1, 0])
14576 |> close()
14577 |> extrude(length = 10)
14578part = subtract(boxSolid, tools = [cutSolid])
14579 |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14580";
14581 let program = Program::parse(source).unwrap().0.unwrap();
14582 let mut frontend = FrontendState::new();
14583 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14584 match frontend.hack_set_program(&ctx, program).await.unwrap() {
14585 SetProgramOutcome::Success { .. } => {}
14586 SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14587 }
14588
14589 let sweep_call_start = source.find("extrude").unwrap();
14590 let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14591 let part_call_start = source.find("subtract").unwrap();
14592 let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14593 let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14594 let composite_range = [part_call_start, part_call_end, 0].into();
14595
14596 let cap_object = frontend
14597 .scene_graph
14598 .objects
14599 .iter()
14600 .find(|object| {
14601 matches!(
14602 &object.kind,
14603 ObjectKind::Cap(crate::frontend::api::Cap {
14604 kind: crate::frontend::api::CapKind::End,
14605 source,
14606 ..
14607 }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14608 )
14609 })
14610 .expect("expected end cap object to trace through subtract and original extrude");
14611
14612 let mut ast = frontend.program.ast.clone();
14613 let cap_expr = sketch_on_ast_expr(
14614 &mut ast,
14615 &frontend.scene_graph,
14616 &frontend.solid_references,
14617 &Plane::Object(cap_object.id),
14618 )
14619 .unwrap();
14620 let cap_face_decl = ast::VariableDeclaration::new(
14621 ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14622 ast::ItemVisibility::Default,
14623 ast::VariableKind::Const,
14624 );
14625 ast.body
14626 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14627 cap_face_decl,
14628 ))));
14629 let generated_source = source_from_ast(&ast);
14630
14631 assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14632 assert!(!generated_source.contains("faceOf(boxSolid"));
14633 let ast::Expr::CallExpressionKw(call) = cap_expr else {
14634 panic!("expected faceOf call");
14635 };
14636 assert_eq!(call.callee.name.name, "faceOf");
14637 let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14638 panic!("expected solid name");
14639 };
14640 assert_eq!(solid_name.name.name, "part");
14641 let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14642 panic!("expected face name");
14643 };
14644 assert_eq!(face_name.name.name, "END");
14645
14646 ctx.close().await;
14647 }
14648
14649 #[tokio::test(flavor = "multi_thread")]
14650 async fn test_sketch_on_plane_incremental() {
14651 let initial_source = "\
14652len = 2mm
14653cube = startSketchOn(XY)
14654 |> startProfile(at = [0, 0])
14655 |> line(end = [len, 0], tag = $side)
14656 |> line(end = [0, len])
14657 |> line(end = [-len, 0])
14658 |> line(end = [0, -len])
14659 |> close()
14660 |> extrude(length = len)
14661
14662plane = planeOf(cube, face = side)
14663";
14664
14665 let program = Program::parse(initial_source).unwrap().0.unwrap();
14666
14667 let mut frontend = FrontendState::new();
14668
14669 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14670 let mock_ctx = ExecutorContext::new_mock(None).await;
14671 let version = Version(0);
14672
14673 frontend.hack_set_program(&ctx, program).await.unwrap();
14674 let plane_object = frontend
14676 .scene_graph
14677 .objects
14678 .iter()
14679 .rev()
14680 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14681 .unwrap();
14682 let plane_id = plane_object.id;
14683
14684 let sketch_args = SketchCtor {
14685 on: Plane::Object(plane_id),
14686 };
14687 let (src_delta, scene_delta, sketch_id) = frontend
14688 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14689 .await
14690 .unwrap();
14691 insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14692 assert_eq!(sketch_id, ObjectId(2));
14693 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14694 let sketch_object = &scene_delta.new_graph.objects[2];
14695 assert_eq!(sketch_object.id, ObjectId(2));
14696 assert_eq!(
14697 sketch_object.kind,
14698 ObjectKind::Sketch(Sketch {
14699 args: SketchCtor {
14700 on: Plane::Object(plane_id),
14701 },
14702 plane: plane_id,
14703 segments: vec![],
14704 constraints: vec![],
14705 })
14706 );
14707 assert_eq!(scene_delta.new_graph.objects.len(), 9);
14708
14709 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14710 assert_eq!(plane_object.id, plane_id);
14711 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14712
14713 ctx.close().await;
14714 mock_ctx.close().await;
14715 }
14716
14717 #[tokio::test(flavor = "multi_thread")]
14718 async fn test_new_sketch_uses_unique_variable_name() {
14719 let initial_source = "\
14720sketch1 = sketch(on = XY) {
14721}
14722";
14723
14724 let program = Program::parse(initial_source).unwrap().0.unwrap();
14725
14726 let mut frontend = FrontendState::new();
14727 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14728 let version = Version(0);
14729
14730 frontend.hack_set_program(&ctx, program).await.unwrap();
14731
14732 let sketch_args = SketchCtor {
14733 on: Plane::Default(PlaneName::Yz),
14734 };
14735 let (src_delta, _, _) = frontend
14736 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14737 .await
14738 .unwrap();
14739
14740 insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14741
14742 ctx.close().await;
14743 }
14744
14745 #[tokio::test(flavor = "multi_thread")]
14746 async fn test_new_sketch_twice_using_same_plane() {
14747 let initial_source = "\
14748sketch1 = sketch(on = XY) {
14749}
14750";
14751
14752 let program = Program::parse(initial_source).unwrap().0.unwrap();
14753
14754 let mut frontend = FrontendState::new();
14755 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14756 let version = Version(0);
14757
14758 frontend.hack_set_program(&ctx, program).await.unwrap();
14759
14760 let sketch_args = SketchCtor {
14761 on: Plane::Default(PlaneName::Xy),
14762 };
14763 let (src_delta, _, _) = frontend
14764 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14765 .await
14766 .unwrap();
14767
14768 insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14769
14770 ctx.close().await;
14771 }
14772
14773 #[tokio::test(flavor = "multi_thread")]
14774 async fn test_sketch_mode_reuses_cached_on_expression() {
14775 let initial_source = "\
14776width = 2mm
14777sketch(on = offsetPlane(XY, offset = width)) {
14778 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14779 distance([line1.start, line1.end]) == width
14780}
14781";
14782 let program = Program::parse(initial_source).unwrap().0.unwrap();
14783
14784 let mut frontend = FrontendState::new();
14785 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14786 let mock_ctx = ExecutorContext::new_mock(None).await;
14787 let version = Version(0);
14788 let project_id = ProjectId(0);
14789 let file_id = FileId(0);
14790
14791 frontend.hack_set_program(&ctx, program).await.unwrap();
14792 let initial_object_count = frontend.scene_graph.objects.len();
14793 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14794 .expect("Expected sketch object to exist")
14795 .id;
14796
14797 let scene_delta = frontend
14800 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14801 .await
14802 .unwrap();
14803 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14804
14805 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14808 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14809
14810 ctx.close().await;
14811 mock_ctx.close().await;
14812 }
14813
14814 #[tokio::test(flavor = "multi_thread")]
14815 async fn test_edit_sketch_nested_in_pipe() {
14816 clear_mem_cache().await;
14817 let source = r#"
14818profile = sketch(on = XY) {
14819 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14820}
14821 |> translate(x = 2mm)
14822"#;
14823 let program = Program::parse_no_errs(source).unwrap();
14824 let mut frontend = FrontendState::new();
14825 let mock_ctx = ExecutorContext::new_mock(None).await;
14826 let version = Version(0);
14827
14828 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14829 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14830 .expect("Expected piped sketch object")
14831 .id;
14832
14833 let scene_delta = frontend
14834 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14835 .await
14836 .unwrap();
14837 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14838 assert!(
14839 scene_delta
14840 .new_graph
14841 .objects
14842 .iter()
14843 .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14844 "Expected the piped sketch's segments to be present in sketch mode"
14845 );
14846
14847 clear_mem_cache().await;
14848 mock_ctx.close().await;
14849 }
14850
14851 #[tokio::test(flavor = "multi_thread")]
14852 async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14853 clear_mem_cache().await;
14854 let source = r#"
14855useFirstProfile = true
14856
14857profile = if useFirstProfile {
14858 sketch(on = XY) {
14859 line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14860 }
14861} else {
14862 sketch(on = XY) {
14863 line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14864 }
14865}
14866"#;
14867 let program = Program::parse_no_errs(source).unwrap();
14868 let mut frontend = FrontendState::new();
14869 let mock_ctx = ExecutorContext::new_mock(None).await;
14870 let version = Version(0);
14871
14872 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14873 let sketch_object =
14874 find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14875 let sketch_id = sketch_object.id;
14876 let sketch = expect_sketch(sketch_object);
14877 let line_end_id = *sketch
14878 .segments
14879 .get(1)
14880 .expect("Expected the active branch's line end point");
14881
14882 let scene_delta = frontend
14883 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14884 .await
14885 .unwrap();
14886 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14887
14888 let segments = vec![ExistingSegmentCtor {
14889 id: line_end_id,
14890 ctor: SegmentCtor::Point(PointCtor {
14891 position: Point2d {
14892 x: Expr::Var(Number {
14893 value: 30.0,
14894 units: NumericSuffix::Mm,
14895 }),
14896 y: Expr::Var(Number {
14897 value: 15.0,
14898 units: NumericSuffix::Mm,
14899 }),
14900 },
14901 }),
14902 }];
14903 let (source_delta, _) = frontend
14904 .edit_segments(&mock_ctx, version, sketch_id, segments)
14905 .await
14906 .unwrap();
14907 assert!(
14908 source_delta
14909 .text
14910 .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14911 "Expected the active branch's dragged variables to be updated:\n{}",
14912 source_delta.text
14913 );
14914 assert!(
14915 source_delta
14916 .text
14917 .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14918 "Expected the inactive branch to remain unchanged:\n{}",
14919 source_delta.text
14920 );
14921
14922 clear_mem_cache().await;
14923 mock_ctx.close().await;
14924 }
14925
14926 #[tokio::test(flavor = "multi_thread")]
14927 async fn test_multiple_sketch_blocks() {
14928 let initial_source = "\
14929// Cube that requires the engine.
14930width = 2
14931sketch001 = startSketchOn(XY)
14932profile001 = startProfile(sketch001, at = [0, 0])
14933 |> yLine(length = width, tag = $seg1)
14934 |> xLine(length = width)
14935 |> yLine(length = -width)
14936 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14937 |> close()
14938extrude001 = extrude(profile001, length = width)
14939
14940// Get a value that requires the engine.
14941x = segLen(seg1)
14942
14943// Triangle with side length 2*x.
14944sketch(on = XY) {
14945 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14946 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14947 coincident([line1.end, line2.start])
14948 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14949 coincident([line2.end, line3.start])
14950 coincident([line3.end, line1.start])
14951 equalLength([line3, line1])
14952 equalLength([line1, line2])
14953 distance([line1.start, line1.end]) == 2*x
14954}
14955
14956// Line segment with length x.
14957sketch2 = sketch(on = XY) {
14958 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14959 distance([line1.start, line1.end]) == x
14960}
14961";
14962
14963 let program = Program::parse(initial_source).unwrap().0.unwrap();
14964
14965 let mut frontend = FrontendState::new();
14966
14967 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14968 let mock_ctx = ExecutorContext::new_mock(None).await;
14969 let version = Version(0);
14970 let project_id = ProjectId(0);
14971 let file_id = FileId(0);
14972
14973 frontend.hack_set_program(&ctx, program).await.unwrap();
14974 let sketch_objects = frontend
14975 .scene_graph
14976 .objects
14977 .iter()
14978 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14979 .collect::<Vec<_>>();
14980 let sketch1_id = sketch_objects.first().unwrap().id;
14981 let sketch2_id = sketch_objects.get(1).unwrap().id;
14982 let point1_id = ObjectId(sketch1_id.0 + 1);
14984 let point2_id = ObjectId(sketch2_id.0 + 1);
14986
14987 let scene_delta = frontend
14996 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14997 .await
14998 .unwrap();
14999 assert_eq!(
15000 scene_delta.new_graph.objects.len(),
15001 18,
15002 "{:#?}",
15003 scene_delta.new_graph.objects
15004 );
15005
15006 let point_ctor = PointCtor {
15008 position: Point2d {
15009 x: Expr::Var(Number {
15010 value: 1.0,
15011 units: NumericSuffix::Mm,
15012 }),
15013 y: Expr::Var(Number {
15014 value: 2.0,
15015 units: NumericSuffix::Mm,
15016 }),
15017 },
15018 };
15019 let segments = vec![ExistingSegmentCtor {
15020 id: point1_id,
15021 ctor: SegmentCtor::Point(point_ctor),
15022 }];
15023 let (src_delta, _) = frontend
15024 .edit_segments(&mock_ctx, version, sketch1_id, segments)
15025 .await
15026 .unwrap();
15027 insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15029 let edited_sketch1_source = src_delta.text.clone();
15030
15031 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15033 assert_eq!(src_delta.text, edited_sketch1_source);
15034 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15042 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15043
15044 let scene_delta = frontend
15052 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15053 .await
15054 .unwrap();
15055 assert_eq!(
15056 scene_delta.new_graph.objects.len(),
15057 24,
15058 "{:#?}",
15059 scene_delta.new_graph.objects
15060 );
15061
15062 let point_ctor = PointCtor {
15064 position: Point2d {
15065 x: Expr::Var(Number {
15066 value: 3.0,
15067 units: NumericSuffix::Mm,
15068 }),
15069 y: Expr::Var(Number {
15070 value: 4.0,
15071 units: NumericSuffix::Mm,
15072 }),
15073 },
15074 };
15075 let segments = vec![ExistingSegmentCtor {
15076 id: point2_id,
15077 ctor: SegmentCtor::Point(point_ctor),
15078 }];
15079 let (src_delta, _) = frontend
15080 .edit_segments(&mock_ctx, version, sketch2_id, segments)
15081 .await
15082 .unwrap();
15083 insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15085 let edited_sketch2_source = src_delta.text.clone();
15086
15087 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15089 assert_eq!(src_delta.text, edited_sketch2_source);
15090
15091 ctx.close().await;
15092 mock_ctx.close().await;
15093 }
15094
15095 #[tokio::test(flavor = "multi_thread")]
15096 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15097 clear_mem_cache().await;
15098
15099 let source = r#"sketch001 = sketch(on = XZ) {
15100 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15101}
15102sketch002 = sketch(on = XY) {
15103 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15104 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15105 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15106 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15107 coincident([line1.end, line2.start])
15108 coincident([line2.end, line3.start])
15109 coincident([line3.end, line4.start])
15110 coincident([line4.end, line1.start])
15111 parallel([line2, line4])
15112 parallel([line3, line1])
15113 perpendicular([line1, line2])
15114 horizontal(line3)
15115 coincident([line1.start, ORIGIN])
15116}
15117"#;
15118
15119 let program = Program::parse(source).unwrap().0.unwrap();
15120 let mut frontend = FrontendState::new();
15121 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15122 let mock_ctx = ExecutorContext::new_mock(None).await;
15123 let version = Version(0);
15124 let project_id = ProjectId(0);
15125 let file_id = FileId(0);
15126
15127 frontend.hack_set_program(&ctx, program).await.unwrap();
15128 let sketch_objects = frontend
15129 .scene_graph
15130 .objects
15131 .iter()
15132 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15133 .collect::<Vec<_>>();
15134 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15135
15136 let sketch1_id = sketch_objects[0].id;
15137 let sketch2_id = sketch_objects[1].id;
15138
15139 frontend
15140 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15141 .await
15142 .unwrap();
15143 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15144
15145 let scene_delta = frontend
15146 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15147 .await
15148 .unwrap();
15149 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15150
15151 clear_mem_cache().await;
15152 ctx.close().await;
15153 mock_ctx.close().await;
15154 }
15155
15156 #[tokio::test(flavor = "multi_thread")]
15161 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15162 let initial_source = "@settings(defaultLengthUnit = mm)
15164
15165sketch001 = sketch(on = XY) {
15166 point(at = [1in, 2in])
15167}
15168";
15169
15170 let program = Program::parse(initial_source).unwrap().0.unwrap();
15171 let mut frontend = FrontendState::new();
15172
15173 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15174 let mock_ctx = ExecutorContext::new_mock(None).await;
15175 let version = Version(0);
15176 let project_id = ProjectId(0);
15177 let file_id = FileId(0);
15178
15179 frontend.hack_set_program(&ctx, program).await.unwrap();
15180 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15181 let sketch_id = sketch_object.id;
15182
15183 frontend
15185 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15186 .await
15187 .unwrap();
15188
15189 let point_ctor = PointCtor {
15191 position: Point2d {
15192 x: Expr::Number(Number {
15193 value: 5.0,
15194 units: NumericSuffix::Mm,
15195 }),
15196 y: Expr::Number(Number {
15197 value: 6.0,
15198 units: NumericSuffix::Mm,
15199 }),
15200 },
15201 };
15202 let segment = SegmentCtor::Point(point_ctor);
15203 let (src_delta, scene_delta) = frontend
15204 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15205 .await
15206 .unwrap();
15207 assert!(
15209 src_delta.text.contains("point(at = [5mm, 6mm])"),
15210 "Expected new point in source, got: {}",
15211 src_delta.text
15212 );
15213 assert!(!scene_delta.new_objects.is_empty());
15214
15215 ctx.close().await;
15216 mock_ctx.close().await;
15217 }
15218
15219 #[tokio::test(flavor = "multi_thread")]
15220 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15221 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15222
15223 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15224 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15225
15226 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15227 assert!(
15228 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15229 "Expected experimental settings to be added to source"
15230 );
15231 }
15232
15233 #[tokio::test(flavor = "multi_thread")]
15234 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15235 let initial_source = "@settings(defaultLengthUnit = mm)
15237
15238s = sketch(on = XY) {}
15239";
15240
15241 let program = Program::parse(initial_source).unwrap().0.unwrap();
15242 let mut frontend = FrontendState::new();
15243
15244 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15245 let mock_ctx = ExecutorContext::new_mock(None).await;
15246 let version = Version(0);
15247
15248 frontend.hack_set_program(&ctx, program).await.unwrap();
15249 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15250 let sketch_id = sketch_object.id;
15251
15252 let line_ctor = LineCtor {
15253 start: Point2d {
15254 x: Expr::Number(Number {
15255 value: 0.0,
15256 units: NumericSuffix::Mm,
15257 }),
15258 y: Expr::Number(Number {
15259 value: 0.0,
15260 units: NumericSuffix::Mm,
15261 }),
15262 },
15263 end: Point2d {
15264 x: Expr::Number(Number {
15265 value: 10.0,
15266 units: NumericSuffix::Mm,
15267 }),
15268 y: Expr::Number(Number {
15269 value: 10.0,
15270 units: NumericSuffix::Mm,
15271 }),
15272 },
15273 construction: None,
15274 };
15275 let segment = SegmentCtor::Line(line_ctor);
15276 let (src_delta, scene_delta) = frontend
15277 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15278 .await
15279 .unwrap();
15280 assert!(
15281 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15282 "Expected line in source, got: {}",
15283 src_delta.text
15284 );
15285 assert_eq!(scene_delta.new_objects.len(), 3);
15287
15288 ctx.close().await;
15289 mock_ctx.close().await;
15290 }
15291
15292 #[tokio::test(flavor = "multi_thread")]
15293 async fn test_extra_newlines_between_operations_edit_line() {
15294 let initial_source = "@settings(defaultLengthUnit = mm)
15296
15297sketch001 = sketch(on = XY) {
15298
15299 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15300
15301}
15302";
15303
15304 let program = Program::parse(initial_source).unwrap().0.unwrap();
15305 let mut frontend = FrontendState::new();
15306
15307 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15308 let mock_ctx = ExecutorContext::new_mock(None).await;
15309 let version = Version(0);
15310 let project_id = ProjectId(0);
15311 let file_id = FileId(0);
15312
15313 frontend.hack_set_program(&ctx, program).await.unwrap();
15314 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15315 let sketch_id = sketch_object.id;
15316 let sketch = expect_sketch(sketch_object);
15317
15318 let line_id = sketch
15320 .segments
15321 .iter()
15322 .copied()
15323 .find(|seg_id| {
15324 matches!(
15325 &frontend.scene_graph.objects[seg_id.0].kind,
15326 ObjectKind::Segment {
15327 segment: Segment::Line(_)
15328 }
15329 )
15330 })
15331 .expect("Expected a line segment in sketch");
15332
15333 frontend
15335 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15336 .await
15337 .unwrap();
15338
15339 let line_ctor = LineCtor {
15341 start: Point2d {
15342 x: Expr::Var(Number {
15343 value: 1.0,
15344 units: NumericSuffix::Mm,
15345 }),
15346 y: Expr::Var(Number {
15347 value: 2.0,
15348 units: NumericSuffix::Mm,
15349 }),
15350 },
15351 end: Point2d {
15352 x: Expr::Var(Number {
15353 value: 13.0,
15354 units: NumericSuffix::Mm,
15355 }),
15356 y: Expr::Var(Number {
15357 value: 14.0,
15358 units: NumericSuffix::Mm,
15359 }),
15360 },
15361 construction: None,
15362 };
15363 let segments = vec![ExistingSegmentCtor {
15364 id: line_id,
15365 ctor: SegmentCtor::Line(line_ctor),
15366 }];
15367 let (src_delta, _scene_delta) = frontend
15368 .edit_segments(&mock_ctx, version, sketch_id, segments)
15369 .await
15370 .unwrap();
15371 assert!(
15372 src_delta
15373 .text
15374 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15375 "Expected edited line in source, got: {}",
15376 src_delta.text
15377 );
15378
15379 ctx.close().await;
15380 mock_ctx.close().await;
15381 }
15382
15383 #[tokio::test(flavor = "multi_thread")]
15384 async fn test_extra_newlines_delete_segment() {
15385 let initial_source = "@settings(defaultLengthUnit = mm)
15387
15388sketch001 = sketch(on = XY) {
15389 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15390}
15391";
15392
15393 let program = Program::parse(initial_source).unwrap().0.unwrap();
15394 let mut frontend = FrontendState::new();
15395
15396 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15397 let mock_ctx = ExecutorContext::new_mock(None).await;
15398 let version = Version(0);
15399
15400 frontend.hack_set_program(&ctx, program).await.unwrap();
15401 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15402 let sketch_id = sketch_object.id;
15403 let sketch = expect_sketch(sketch_object);
15404
15405 assert_eq!(sketch.segments.len(), 3);
15407 let circle_id = sketch.segments[2];
15408
15409 let (src_delta, scene_delta) = frontend
15411 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15412 .await
15413 .unwrap();
15414 assert!(
15415 src_delta.text.contains("sketch(on = XY) {"),
15416 "Expected sketch block in source, got: {}",
15417 src_delta.text
15418 );
15419 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15420 let new_sketch = expect_sketch(new_sketch_object);
15421 assert_eq!(new_sketch.segments.len(), 0);
15422
15423 ctx.close().await;
15424 mock_ctx.close().await;
15425 }
15426
15427 #[tokio::test(flavor = "multi_thread")]
15428 async fn test_unformatted_source_add_arc() {
15429 let initial_source = "@settings(defaultLengthUnit = mm)
15431
15432sketch001 = sketch(on = XY) {
15433}
15434";
15435
15436 let program = Program::parse(initial_source).unwrap().0.unwrap();
15437 let mut frontend = FrontendState::new();
15438
15439 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15440 let mock_ctx = ExecutorContext::new_mock(None).await;
15441 let version = Version(0);
15442
15443 frontend.hack_set_program(&ctx, program).await.unwrap();
15444 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15445 let sketch_id = sketch_object.id;
15446
15447 let arc_ctor = ArcCtor {
15448 start: Point2d {
15449 x: Expr::Var(Number {
15450 value: 5.0,
15451 units: NumericSuffix::Mm,
15452 }),
15453 y: Expr::Var(Number {
15454 value: 0.0,
15455 units: NumericSuffix::Mm,
15456 }),
15457 },
15458 end: Point2d {
15459 x: Expr::Var(Number {
15460 value: 0.0,
15461 units: NumericSuffix::Mm,
15462 }),
15463 y: Expr::Var(Number {
15464 value: 5.0,
15465 units: NumericSuffix::Mm,
15466 }),
15467 },
15468 center: Point2d {
15469 x: Expr::Var(Number {
15470 value: 0.0,
15471 units: NumericSuffix::Mm,
15472 }),
15473 y: Expr::Var(Number {
15474 value: 0.0,
15475 units: NumericSuffix::Mm,
15476 }),
15477 },
15478 direction: None,
15479 construction: None,
15480 };
15481 let segment = SegmentCtor::Arc(arc_ctor);
15482 let (src_delta, scene_delta) = frontend
15483 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15484 .await
15485 .unwrap();
15486 assert!(
15487 src_delta
15488 .text
15489 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15490 "Expected arc in source, got: {}",
15491 src_delta.text
15492 );
15493 assert!(!scene_delta.new_objects.is_empty());
15494
15495 ctx.close().await;
15496 mock_ctx.close().await;
15497 }
15498
15499 #[tokio::test(flavor = "multi_thread")]
15500 async fn test_arc_direction_flows_to_source() {
15501 let initial_source = "@settings(defaultLengthUnit = mm)
15502
15503sketch001 = sketch(on = XY) {
15504}
15505";
15506
15507 let program = Program::parse(initial_source).unwrap().0.unwrap();
15508 let mut frontend = FrontendState::new();
15509
15510 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15511 let mock_ctx = ExecutorContext::new_mock(None).await;
15512 let version = Version(0);
15513
15514 frontend.hack_set_program(&ctx, program).await.unwrap();
15515 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15516 let sketch_id = sketch_object.id;
15517
15518 let point = |x: f64, y: f64| Point2d {
15519 x: Expr::Var(Number {
15520 value: x,
15521 units: NumericSuffix::Mm,
15522 }),
15523 y: Expr::Var(Number {
15524 value: y,
15525 units: NumericSuffix::Mm,
15526 }),
15527 };
15528
15529 let arc_ctor = ArcCtor {
15531 start: point(5.0, 0.0),
15532 end: point(0.0, 5.0),
15533 center: point(0.0, 0.0),
15534 direction: Some(ArcDirection::Cw),
15535 construction: None,
15536 };
15537 let (src_delta, scene_delta) = frontend
15538 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15539 .await
15540 .unwrap();
15541 assert!(
15542 src_delta.text.contains("direction = CW"),
15543 "Expected direction = CW in source, got: {}",
15544 src_delta.text
15545 );
15546 let arc_id = *scene_delta.new_objects.last().unwrap();
15548
15549 let edited_ctor = ArcCtor {
15553 start: point(0.0, -5.0),
15554 end: point(0.0, 5.0),
15555 center: point(0.0, 0.0),
15556 direction: Some(ArcDirection::Cw),
15557 construction: None,
15558 };
15559 let (src_delta, _scene_delta) = frontend
15560 .edit_segments(
15561 &mock_ctx,
15562 version,
15563 sketch_id,
15564 vec![ExistingSegmentCtor {
15565 id: arc_id,
15566 ctor: SegmentCtor::Arc(edited_ctor),
15567 }],
15568 )
15569 .await
15570 .unwrap();
15571 assert!(
15572 src_delta.text.contains("start = [var 0mm, var -5mm]"),
15573 "Expected edited start point in source, got: {}",
15574 src_delta.text
15575 );
15576 assert!(
15577 src_delta.text.contains("direction = CW"),
15578 "Expected direction = CW to be preserved in source, got: {}",
15579 src_delta.text
15580 );
15581
15582 let edited_ctor = ArcCtor {
15585 start: point(0.0, -5.0),
15586 end: point(0.0, 5.0),
15587 center: point(0.0, 0.0),
15588 direction: Some(ArcDirection::Ccw),
15589 construction: None,
15590 };
15591 let (src_delta, _scene_delta) = frontend
15592 .edit_segments(
15593 &mock_ctx,
15594 version,
15595 sketch_id,
15596 vec![ExistingSegmentCtor {
15597 id: arc_id,
15598 ctor: SegmentCtor::Arc(edited_ctor),
15599 }],
15600 )
15601 .await
15602 .unwrap();
15603 assert!(
15604 !src_delta.text.contains("direction"),
15605 "Expected direction argument to be removed from source, got: {}",
15606 src_delta.text
15607 );
15608
15609 ctx.close().await;
15610 mock_ctx.close().await;
15611 }
15612
15613 #[tokio::test(flavor = "multi_thread")]
15614 async fn test_extra_newlines_add_circle() {
15615 let initial_source = "@settings(defaultLengthUnit = mm)
15617
15618sketch001 = sketch(on = XY) {
15619}
15620";
15621
15622 let program = Program::parse(initial_source).unwrap().0.unwrap();
15623 let mut frontend = FrontendState::new();
15624
15625 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15626 let mock_ctx = ExecutorContext::new_mock(None).await;
15627 let version = Version(0);
15628
15629 frontend.hack_set_program(&ctx, program).await.unwrap();
15630 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15631 let sketch_id = sketch_object.id;
15632
15633 let circle_ctor = CircleCtor {
15634 start: Point2d {
15635 x: Expr::Var(Number {
15636 value: 5.0,
15637 units: NumericSuffix::Mm,
15638 }),
15639 y: Expr::Var(Number {
15640 value: 0.0,
15641 units: NumericSuffix::Mm,
15642 }),
15643 },
15644 center: Point2d {
15645 x: Expr::Var(Number {
15646 value: 0.0,
15647 units: NumericSuffix::Mm,
15648 }),
15649 y: Expr::Var(Number {
15650 value: 0.0,
15651 units: NumericSuffix::Mm,
15652 }),
15653 },
15654 construction: None,
15655 };
15656 let segment = SegmentCtor::Circle(circle_ctor);
15657 let (src_delta, scene_delta) = frontend
15658 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15659 .await
15660 .unwrap();
15661 assert!(
15662 src_delta
15663 .text
15664 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15665 "Expected circle in source, got: {}",
15666 src_delta.text
15667 );
15668 assert!(!scene_delta.new_objects.is_empty());
15669
15670 ctx.close().await;
15671 mock_ctx.close().await;
15672 }
15673
15674 #[tokio::test(flavor = "multi_thread")]
15675 async fn test_extra_newlines_add_constraint() {
15676 let initial_source = "@settings(defaultLengthUnit = mm)
15678
15679sketch001 = sketch(on = XY) {
15680 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15681 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15682}
15683";
15684
15685 let program = Program::parse(initial_source).unwrap().0.unwrap();
15686 let mut frontend = FrontendState::new();
15687
15688 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15689 let mock_ctx = ExecutorContext::new_mock(None).await;
15690 let version = Version(0);
15691 let project_id = ProjectId(0);
15692 let file_id = FileId(0);
15693
15694 frontend.hack_set_program(&ctx, program).await.unwrap();
15695 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15696 let sketch_id = sketch_object.id;
15697 let sketch = expect_sketch(sketch_object);
15698
15699 let line_ids: Vec<ObjectId> = sketch
15701 .segments
15702 .iter()
15703 .copied()
15704 .filter(|seg_id| {
15705 matches!(
15706 &frontend.scene_graph.objects[seg_id.0].kind,
15707 ObjectKind::Segment {
15708 segment: Segment::Line(_)
15709 }
15710 )
15711 })
15712 .collect();
15713 assert_eq!(line_ids.len(), 2, "Expected two line segments");
15714
15715 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15716 let ObjectKind::Segment {
15717 segment: Segment::Line(line1_data),
15718 } = &line1.kind
15719 else {
15720 panic!("Expected line");
15721 };
15722 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15723 let ObjectKind::Segment {
15724 segment: Segment::Line(line2_data),
15725 } = &line2.kind
15726 else {
15727 panic!("Expected line");
15728 };
15729
15730 let constraint = Constraint::Coincident(Coincident {
15732 segments: vec![line1_data.end.into(), line2_data.start.into()],
15733 });
15734
15735 frontend
15737 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15738 .await
15739 .unwrap();
15740 let (src_delta, _scene_delta) = frontend
15741 .add_constraint(&mock_ctx, version, sketch_id, constraint)
15742 .await
15743 .unwrap();
15744 assert!(
15745 src_delta.text.contains("coincident("),
15746 "Expected coincident constraint in source, got: {}",
15747 src_delta.text
15748 );
15749
15750 ctx.close().await;
15751 mock_ctx.close().await;
15752 }
15753
15754 #[tokio::test(flavor = "multi_thread")]
15755 async fn test_extra_newlines_add_line_then_edit_line() {
15756 let initial_source = "@settings(defaultLengthUnit = mm)
15758
15759sketch001 = sketch(on = XY) {
15760}
15761";
15762
15763 let program = Program::parse(initial_source).unwrap().0.unwrap();
15764 let mut frontend = FrontendState::new();
15765
15766 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15767 let mock_ctx = ExecutorContext::new_mock(None).await;
15768 let version = Version(0);
15769
15770 frontend.hack_set_program(&ctx, program).await.unwrap();
15771 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15772 let sketch_id = sketch_object.id;
15773
15774 let line_ctor = LineCtor {
15776 start: Point2d {
15777 x: Expr::Number(Number {
15778 value: 0.0,
15779 units: NumericSuffix::Mm,
15780 }),
15781 y: Expr::Number(Number {
15782 value: 0.0,
15783 units: NumericSuffix::Mm,
15784 }),
15785 },
15786 end: Point2d {
15787 x: Expr::Number(Number {
15788 value: 10.0,
15789 units: NumericSuffix::Mm,
15790 }),
15791 y: Expr::Number(Number {
15792 value: 10.0,
15793 units: NumericSuffix::Mm,
15794 }),
15795 },
15796 construction: None,
15797 };
15798 let segment = SegmentCtor::Line(line_ctor);
15799 let (src_delta, scene_delta) = frontend
15800 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15801 .await
15802 .unwrap();
15803 assert!(
15804 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15805 "Expected line in source after add, got: {}",
15806 src_delta.text
15807 );
15808 let line_id = *scene_delta.new_objects.last().unwrap();
15810
15811 let line_ctor = LineCtor {
15813 start: Point2d {
15814 x: Expr::Number(Number {
15815 value: 1.0,
15816 units: NumericSuffix::Mm,
15817 }),
15818 y: Expr::Number(Number {
15819 value: 2.0,
15820 units: NumericSuffix::Mm,
15821 }),
15822 },
15823 end: Point2d {
15824 x: Expr::Number(Number {
15825 value: 13.0,
15826 units: NumericSuffix::Mm,
15827 }),
15828 y: Expr::Number(Number {
15829 value: 14.0,
15830 units: NumericSuffix::Mm,
15831 }),
15832 },
15833 construction: None,
15834 };
15835 let segments = vec![ExistingSegmentCtor {
15836 id: line_id,
15837 ctor: SegmentCtor::Line(line_ctor),
15838 }];
15839 let (src_delta, scene_delta) = frontend
15840 .edit_segments(&mock_ctx, version, sketch_id, segments)
15841 .await
15842 .unwrap();
15843 assert!(
15844 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15845 "Expected edited line in source, got: {}",
15846 src_delta.text
15847 );
15848 assert_eq!(scene_delta.new_objects, vec![]);
15849
15850 ctx.close().await;
15851 mock_ctx.close().await;
15852 }
15853
15854 #[test]
15855 fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15856 let code = "\
15865foo1 = 1
15866sk = sketch() {
15867 p = var 1.5
15868}
158697 + 8
15870";
15871 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15872 let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15873 panic!("expected an expression statement");
15874 };
15875 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15876 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15877 &mut ast,
15878 &source_ref,
15879 AstMutateCommand::AddVariableDeclaration {
15880 prefix: "foo".to_owned(),
15881 },
15882 )
15883 .unwrap();
15884 let AstMutateCommandReturn::Name(name) = cmd_return else {
15885 panic!("expected a generated name");
15886 };
15887 assert_eq!(name, "foo2");
15888 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15889 panic!("expected the expression statement to become a variable declaration");
15890 };
15891 assert_eq!(decl.name(), "foo2");
15892 }
15893
15894 fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15896 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15897 panic!("expected a variable declaration");
15898 };
15899 let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15900 panic!("expected a function expression");
15901 };
15902 &func.body
15903 }
15904
15905 fn then_block_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::IfExpression(if_expr) = &decl.declaration.init else {
15912 panic!("expected an if expression");
15913 };
15914 &if_expr.then_val
15915 }
15916
15917 #[test]
15918 fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15919 let code = "\
15924fn build() {
15925 thing1 = 1
15926 10 + 20
15927 return thing1
15928}
15929";
15930 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15931 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15932 panic!("expected an expression statement");
15933 };
15934 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15935 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15936 &mut ast,
15937 &source_ref,
15938 AstMutateCommand::AddVariableDeclaration {
15939 prefix: "thing".to_owned(),
15940 },
15941 )
15942 .unwrap();
15943 let AstMutateCommandReturn::Name(name) = cmd_return else {
15944 panic!("expected a generated name");
15945 };
15946 assert_eq!(name, "thing2");
15947 let body = &function_body_at(&ast, 0).body;
15948 assert_eq!(body.len(), 3);
15949 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
15950 panic!("expected the expression statement to become a variable declaration");
15951 };
15952 assert_eq!(decl.name(), "thing2");
15953 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15955 panic!("expected a variable declaration");
15956 };
15957 assert_eq!(first.name(), "thing1");
15958 assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
15959 }
15960
15961 #[test]
15962 fn test_delete_node_in_function_body_preserves_leading_comment() {
15963 let code = "\
15967fn build() {
15968 a = 1
15969 // keep me
15970 b = 2
15971 return a
15972}
15973";
15974 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15975 let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
15976 panic!("expected a variable declaration");
15977 };
15978 assert_eq!(b_decl.name(), "b");
15979 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
15980 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
15981 let body = &function_body_at(&ast, 0).body;
15982 assert_eq!(body.len(), 2, "expected b to be deleted");
15983 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15984 panic!("expected a variable declaration");
15985 };
15986 assert_eq!(first.name(), "a");
15987 let ast::BodyItem::ReturnStatement(_) = &body[1] else {
15988 panic!("expected the return statement to remain");
15989 };
15990 assert!(
15991 body[1].get_comments().iter().any(|c| c.contains("keep me")),
15992 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
15993 body[1].get_comments()
15994 );
15995 }
15996
15997 #[test]
15998 fn test_add_variable_declaration_in_function_body_ignores_parameters() {
15999 let code = "\
16007fn build(thing2) {
16008 thing1 = 1
16009 10 + 20
16010 return thing1 + thing2
16011}
16012";
16013 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16014 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16015 panic!("expected an expression statement");
16016 };
16017 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16018 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16019 &mut ast,
16020 &source_ref,
16021 AstMutateCommand::AddVariableDeclaration {
16022 prefix: "thing".to_owned(),
16023 },
16024 )
16025 .unwrap();
16026 let AstMutateCommandReturn::Name(name) = cmd_return else {
16027 panic!("expected a generated name");
16028 };
16029 assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16030 }
16031
16032 #[test]
16033 fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16034 let code = "\
16035x = 1
16036y = if x > 0 {
16037 q1 = 1
16038 foo(q1)
16039 q1
16040} else {
16041 2
16042}
16043";
16044 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16045 let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16046 panic!("expected an expression statement");
16047 };
16048 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16049 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16050 &mut ast,
16051 &source_ref,
16052 AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16053 )
16054 .unwrap();
16055 let AstMutateCommandReturn::Name(name) = cmd_return else {
16056 panic!("expected a generated name");
16057 };
16058 assert_eq!(name, "q2");
16059 let body = &then_block_at(&ast, 1).body;
16060 assert_eq!(body.len(), 3);
16061 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16062 panic!("expected the expression statement to become a variable declaration");
16063 };
16064 assert_eq!(decl.name(), "q2");
16065 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16067 panic!("expected a variable declaration");
16068 };
16069 assert_eq!(first.name(), "q1");
16070 assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16071 }
16072
16073 #[test]
16074 fn test_delete_node_in_if_branch_preserves_leading_comment() {
16075 let code = "\
16078y = if true {
16079 a = 1
16080 // keep me
16081 b = 2
16082 a + b
16083} else {
16084 2
16085}
16086";
16087 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16088 let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16089 panic!("expected a variable declaration");
16090 };
16091 assert_eq!(b_decl.name(), "b");
16092 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16093 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16094 let body = &then_block_at(&ast, 0).body;
16095 assert_eq!(body.len(), 2, "expected b to be deleted");
16096 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16097 panic!("expected a variable declaration");
16098 };
16099 assert_eq!(first.name(), "a");
16100 let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16101 panic!("expected the tail expression to remain");
16102 };
16103 assert!(
16104 body[1].get_comments().iter().any(|c| c.contains("keep me")),
16105 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16106 body[1].get_comments()
16107 );
16108 }
16109}