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 filenames,
2066 operations,
2067 artifact_graph,
2068 scene_objects,
2069 source_range_to_object,
2070 var_solutions,
2071 refactor_metadata,
2072 issues: non_fatal,
2073 source_files,
2074 default_planes,
2075 })
2076 }
2077
2078 async fn add_point(
2079 &mut self,
2080 ctx: &ExecutorContext,
2081 sketch: ObjectId,
2082 ctor: PointCtor,
2083 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2084 let at_ast = to_ast_point2d(&ctor.position)
2086 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2087 let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2088 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2089 unlabeled: None,
2090 arguments: vec![ast::LabeledArg {
2091 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2092 arg: at_ast,
2093 }],
2094 digest: None,
2095 non_code_meta: Default::default(),
2096 })));
2097
2098 let sketch_id = sketch;
2100 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2101 #[cfg(target_arch = "wasm32")]
2102 web_sys::console::error_1(
2103 &format!(
2104 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2105 self.scene_graph.objects
2106 )
2107 .into(),
2108 );
2109 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2110 })?;
2111 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2112 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2113 "Object is not a sketch, it is {}",
2114 sketch_object.kind.human_friendly_kind_with_article(),
2115 ))));
2116 };
2117 let mut new_ast = self.program.ast.clone();
2119 let (sketch_block_ref, _) = self
2120 .mutate_ast(
2121 &mut new_ast,
2122 sketch_id,
2123 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2124 )
2125 .map_err(KclErrorWithOutputs::no_outputs)?;
2126 let new_source = source_from_ast(&new_ast);
2128 let new_program = parse_frontend_mutation_source(
2130 &new_source,
2131 "Error parsing KCL source after adding point",
2132 "No AST produced after adding point",
2133 )?;
2134
2135 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2136 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2137 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2138 )))
2139 })?;
2140
2141 self.program = new_program.clone();
2143
2144 let mut truncated_program = new_program;
2146 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2147 .map_err(KclErrorWithOutputs::no_outputs)?;
2148
2149 let outcome = ctx
2151 .run_mock(
2152 &truncated_program,
2153 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2154 )
2155 .await?;
2156
2157 let new_object_ids = {
2158 let make_err =
2159 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2160 let segment_id = outcome
2161 .source_range_to_object
2162 .get(&point_node_ref.range)
2163 .copied()
2164 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2165 let segment_object = outcome
2166 .scene_objects
2167 .get(segment_id.0)
2168 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2169 let ObjectKind::Segment { segment } = &segment_object.kind else {
2170 return Err(make_err(format!(
2171 "Object is not a segment, it is {}",
2172 segment_object.kind.human_friendly_kind_with_article()
2173 )));
2174 };
2175 let Segment::Point(_) = segment else {
2176 return Err(make_err(format!(
2177 "Segment is not a point, it is {}",
2178 segment.human_friendly_kind_with_article()
2179 )));
2180 };
2181 vec![segment_id]
2182 };
2183 let src_delta = SourceDelta { text: new_source };
2184 let outcome = self.update_state_after_exec(outcome, false);
2186 let scene_graph_delta = SceneGraphDelta {
2187 new_graph: self.scene_graph_for_ui(),
2188 invalidates_ids: false,
2189 new_objects: new_object_ids,
2190 exec_outcome: outcome,
2191 };
2192 Ok((src_delta, scene_graph_delta))
2193 }
2194
2195 async fn add_line(
2196 &mut self,
2197 ctx: &ExecutorContext,
2198 sketch: ObjectId,
2199 ctor: LineCtor,
2200 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2201 let start_ast = to_ast_point2d(&ctor.start)
2203 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2204 let end_ast = to_ast_point2d(&ctor.end)
2205 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2206 let mut arguments = vec![
2207 ast::LabeledArg {
2208 label: Some(ast::Identifier::new(LINE_START_PARAM)),
2209 arg: start_ast,
2210 },
2211 ast::LabeledArg {
2212 label: Some(ast::Identifier::new(LINE_END_PARAM)),
2213 arg: end_ast,
2214 },
2215 ];
2216 if ctor.construction == Some(true) {
2218 arguments.push(ast::LabeledArg {
2219 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2220 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2221 value: ast::LiteralValue::Bool(true),
2222 raw: "true".to_string(),
2223 digest: None,
2224 }))),
2225 });
2226 }
2227 let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2228 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2229 unlabeled: None,
2230 arguments,
2231 digest: None,
2232 non_code_meta: Default::default(),
2233 })));
2234
2235 let sketch_id = sketch;
2237 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2238 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2239 })?;
2240 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2241 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2242 "Object is not a sketch, it is {}",
2243 sketch_object.kind.human_friendly_kind_with_article(),
2244 ))));
2245 };
2246 let mut new_ast = self.program.ast.clone();
2248 let (sketch_block_ref, _) = self
2249 .mutate_ast(
2250 &mut new_ast,
2251 sketch_id,
2252 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2253 )
2254 .map_err(KclErrorWithOutputs::no_outputs)?;
2255 let new_source = source_from_ast(&new_ast);
2257 let new_program = parse_frontend_mutation_source(
2259 &new_source,
2260 "Error parsing KCL source after adding line",
2261 "No AST produced after adding line",
2262 )?;
2263
2264 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2265 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2266 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2267 )))
2268 })?;
2269
2270 self.program = new_program.clone();
2272
2273 let mut truncated_program = new_program;
2275 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2276 .map_err(KclErrorWithOutputs::no_outputs)?;
2277
2278 let outcome = ctx
2280 .run_mock(
2281 &truncated_program,
2282 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2283 )
2284 .await?;
2285
2286 let new_object_ids = {
2287 let make_err =
2288 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2289 let segment_id = outcome
2290 .source_range_to_object
2291 .get(&line_node_ref.range)
2292 .copied()
2293 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2294 let segment_object = outcome
2295 .scene_object_by_id(segment_id)
2296 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2297 let ObjectKind::Segment { segment } = &segment_object.kind else {
2298 return Err(make_err(format!(
2299 "Object is not a segment, it is {}",
2300 segment_object.kind.human_friendly_kind_with_article()
2301 )));
2302 };
2303 let Segment::Line(line) = segment else {
2304 return Err(make_err(format!(
2305 "Segment is not a line, it is {}",
2306 segment.human_friendly_kind_with_article()
2307 )));
2308 };
2309 vec![line.start, line.end, segment_id]
2310 };
2311 let src_delta = SourceDelta { text: new_source };
2312 let outcome = self.update_state_after_exec(outcome, false);
2314 let scene_graph_delta = SceneGraphDelta {
2315 new_graph: self.scene_graph_for_ui(),
2316 invalidates_ids: false,
2317 new_objects: new_object_ids,
2318 exec_outcome: outcome,
2319 };
2320 Ok((src_delta, scene_graph_delta))
2321 }
2322
2323 async fn add_arc(
2324 &mut self,
2325 ctx: &ExecutorContext,
2326 sketch: ObjectId,
2327 ctor: ArcCtor,
2328 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2329 let start_ast = to_ast_point2d(&ctor.start)
2331 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2332 let end_ast = to_ast_point2d(&ctor.end)
2333 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2334 let center_ast = to_ast_point2d(&ctor.center)
2335 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2336 let mut arguments = vec![
2337 ast::LabeledArg {
2338 label: Some(ast::Identifier::new(ARC_START_PARAM)),
2339 arg: start_ast,
2340 },
2341 ast::LabeledArg {
2342 label: Some(ast::Identifier::new(ARC_END_PARAM)),
2343 arg: end_ast,
2344 },
2345 ast::LabeledArg {
2346 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2347 arg: center_ast,
2348 },
2349 ];
2350 if ctor.direction == Some(ArcDirection::Cw) {
2353 arguments.push(ast::LabeledArg {
2354 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2355 arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2356 });
2357 }
2358 if ctor.construction == Some(true) {
2360 arguments.push(ast::LabeledArg {
2361 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2362 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2363 value: ast::LiteralValue::Bool(true),
2364 raw: "true".to_string(),
2365 digest: None,
2366 }))),
2367 });
2368 }
2369 let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2370 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2371 unlabeled: None,
2372 arguments,
2373 digest: None,
2374 non_code_meta: Default::default(),
2375 })));
2376
2377 let sketch_id = sketch;
2379 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2380 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2381 })?;
2382 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2383 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2384 "Object is not a sketch, it is {}",
2385 sketch_object.kind.human_friendly_kind_with_article(),
2386 ))));
2387 };
2388 let mut new_ast = self.program.ast.clone();
2390 let (sketch_block_ref, _) = self
2391 .mutate_ast(
2392 &mut new_ast,
2393 sketch_id,
2394 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2395 )
2396 .map_err(KclErrorWithOutputs::no_outputs)?;
2397 let new_source = source_from_ast(&new_ast);
2399 let new_program = parse_frontend_mutation_source(
2401 &new_source,
2402 "Error parsing KCL source after adding arc",
2403 "No AST produced after adding arc",
2404 )?;
2405
2406 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2407 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2408 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2409 )))
2410 })?;
2411
2412 self.program = new_program.clone();
2414
2415 let mut truncated_program = new_program;
2417 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2418 .map_err(KclErrorWithOutputs::no_outputs)?;
2419
2420 let outcome = ctx
2422 .run_mock(
2423 &truncated_program,
2424 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2425 )
2426 .await?;
2427
2428 let new_object_ids = {
2429 let make_err =
2430 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2431 let segment_id = outcome
2432 .source_range_to_object
2433 .get(&arc_node_ref.range)
2434 .copied()
2435 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2436 let segment_object = outcome
2437 .scene_objects
2438 .get(segment_id.0)
2439 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2440 let ObjectKind::Segment { segment } = &segment_object.kind else {
2441 return Err(make_err(format!(
2442 "Object is not a segment, it is {}",
2443 segment_object.kind.human_friendly_kind_with_article()
2444 )));
2445 };
2446 let Segment::Arc(arc) = segment else {
2447 return Err(make_err(format!(
2448 "Segment is not an arc, it is {}",
2449 segment.human_friendly_kind_with_article()
2450 )));
2451 };
2452 vec![arc.start, arc.end, arc.center, segment_id]
2453 };
2454 let src_delta = SourceDelta { text: new_source };
2455 let outcome = self.update_state_after_exec(outcome, false);
2457 let scene_graph_delta = SceneGraphDelta {
2458 new_graph: self.scene_graph_for_ui(),
2459 invalidates_ids: false,
2460 new_objects: new_object_ids,
2461 exec_outcome: outcome,
2462 };
2463 Ok((src_delta, scene_graph_delta))
2464 }
2465
2466 async fn add_circle(
2467 &mut self,
2468 ctx: &ExecutorContext,
2469 sketch: ObjectId,
2470 ctor: CircleCtor,
2471 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2472 let start_ast = to_ast_point2d(&ctor.start)
2474 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2475 let center_ast = to_ast_point2d(&ctor.center)
2476 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2477 let mut arguments = vec![
2478 ast::LabeledArg {
2479 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2480 arg: start_ast,
2481 },
2482 ast::LabeledArg {
2483 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2484 arg: center_ast,
2485 },
2486 ];
2487 if ctor.construction == Some(true) {
2489 arguments.push(ast::LabeledArg {
2490 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2491 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2492 value: ast::LiteralValue::Bool(true),
2493 raw: "true".to_string(),
2494 digest: None,
2495 }))),
2496 });
2497 }
2498 let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2499 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2500 unlabeled: None,
2501 arguments,
2502 digest: None,
2503 non_code_meta: Default::default(),
2504 })));
2505
2506 let sketch_id = sketch;
2508 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2509 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2510 })?;
2511 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2512 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2513 "Object is not a sketch, it is {}",
2514 sketch_object.kind.human_friendly_kind_with_article(),
2515 ))));
2516 };
2517 let mut new_ast = self.program.ast.clone();
2519 let (sketch_block_ref, _) = self
2520 .mutate_ast(
2521 &mut new_ast,
2522 sketch_id,
2523 AstMutateCommand::AddSketchBlockVarDecl {
2524 prefix: CIRCLE_VARIABLE.to_owned(),
2525 expr: circle_ast,
2526 },
2527 )
2528 .map_err(KclErrorWithOutputs::no_outputs)?;
2529 let new_source = source_from_ast(&new_ast);
2531 let new_program = parse_frontend_mutation_source(
2533 &new_source,
2534 "Error parsing KCL source after adding circle",
2535 "No AST produced after adding circle",
2536 )?;
2537
2538 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2539 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2540 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2541 )))
2542 })?;
2543
2544 self.program = new_program.clone();
2546
2547 let mut truncated_program = new_program;
2549 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2550 .map_err(KclErrorWithOutputs::no_outputs)?;
2551
2552 let outcome = ctx
2554 .run_mock(
2555 &truncated_program,
2556 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2557 )
2558 .await?;
2559
2560 let new_object_ids = {
2561 let make_err =
2562 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2563 let segment_id = outcome
2564 .source_range_to_object
2565 .get(&circle_node_ref.range)
2566 .copied()
2567 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2568 let segment_object = outcome
2569 .scene_objects
2570 .get(segment_id.0)
2571 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2572 let ObjectKind::Segment { segment } = &segment_object.kind else {
2573 return Err(make_err(format!(
2574 "Object is not a segment, it is {}",
2575 segment_object.kind.human_friendly_kind_with_article()
2576 )));
2577 };
2578 let Segment::Circle(circle) = segment else {
2579 return Err(make_err(format!(
2580 "Segment is not a circle, it is {}",
2581 segment.human_friendly_kind_with_article()
2582 )));
2583 };
2584 vec![circle.start, circle.center, segment_id]
2585 };
2586 let src_delta = SourceDelta { text: new_source };
2587 let outcome = self.update_state_after_exec(outcome, false);
2589 let scene_graph_delta = SceneGraphDelta {
2590 new_graph: self.scene_graph_for_ui(),
2591 invalidates_ids: false,
2592 new_objects: new_object_ids,
2593 exec_outcome: outcome,
2594 };
2595 Ok((src_delta, scene_graph_delta))
2596 }
2597
2598 async fn add_control_point_spline(
2599 &mut self,
2600 ctx: &ExecutorContext,
2601 sketch: ObjectId,
2602 ctor: ControlPointSplineCtor,
2603 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2604 let new_program = ensure_control_point_spline_experimental_features(&self.program)
2605 .map_err(KclErrorWithOutputs::no_outputs)?;
2606
2607 let points_ast = to_ast_point2d_array(&ctor.points)
2608 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2609 let mut arguments = vec![ast::LabeledArg {
2610 label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2611 arg: points_ast,
2612 }];
2613 if ctor.construction == Some(true) {
2614 arguments.push(ast::LabeledArg {
2615 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2616 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2617 value: ast::LiteralValue::Bool(true),
2618 raw: "true".to_string(),
2619 digest: None,
2620 }))),
2621 });
2622 }
2623 let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2624 callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2625 unlabeled: None,
2626 arguments,
2627 digest: None,
2628 non_code_meta: Default::default(),
2629 })));
2630
2631 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2632 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2633 })?;
2634 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2635 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2636 "Object is not a sketch, it is {}",
2637 sketch_object.kind.human_friendly_kind_with_article(),
2638 ))));
2639 };
2640
2641 let mut new_ast = new_program.ast.clone();
2642 let (sketch_block_ref, _) = self
2643 .mutate_ast(
2644 &mut new_ast,
2645 sketch,
2646 AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2647 )
2648 .map_err(KclErrorWithOutputs::no_outputs)?;
2649 let new_source = source_from_ast(&new_ast);
2650 let new_program = parse_frontend_mutation_source(
2651 &new_source,
2652 "Error parsing KCL source after adding controlPointSpline",
2653 "No AST produced after adding controlPointSpline",
2654 )?;
2655
2656 let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2657 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2658 "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2659 )))
2660 })?;
2661
2662 self.program = new_program.clone();
2663
2664 let mut truncated_program = new_program;
2665 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2666 .map_err(KclErrorWithOutputs::no_outputs)?;
2667
2668 let outcome = ctx
2669 .run_mock(
2670 &truncated_program,
2671 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2672 )
2673 .await?;
2674
2675 let new_object_ids = {
2676 let make_err =
2677 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2678 let segment_id = outcome
2679 .source_range_to_object
2680 .get(&spline_node_ref.range)
2681 .copied()
2682 .ok_or_else(|| {
2683 make_err(format!(
2684 "Source range of controlPointSpline not found: {spline_node_ref:?}"
2685 ))
2686 })?;
2687 let segment_object = outcome
2688 .scene_objects
2689 .get(segment_id.0)
2690 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2691 let ObjectKind::Segment { segment } = &segment_object.kind else {
2692 return Err(make_err(format!(
2693 "Object is not a segment, it is {}",
2694 segment_object.kind.human_friendly_kind_with_article()
2695 )));
2696 };
2697 let Segment::ControlPointSpline(spline) = segment else {
2698 return Err(make_err(format!(
2699 "Segment is not a control point spline, it is {}",
2700 segment.human_friendly_kind_with_article()
2701 )));
2702 };
2703
2704 let mut ids = outcome
2705 .scene_objects
2706 .iter()
2707 .filter_map(|obj| match &obj.kind {
2708 ObjectKind::Segment {
2709 segment: Segment::Line(line),
2710 } if line.owner == Some(segment_id) => Some(obj.id),
2711 _ => None,
2712 })
2713 .collect::<Vec<_>>();
2714 ids.extend(spline.controls.clone());
2715 ids.push(segment_id);
2716 ids
2717 };
2718 let src_delta = SourceDelta { text: new_source };
2719 let outcome = self.update_state_after_exec(outcome, false);
2720 let scene_graph_delta = SceneGraphDelta {
2721 new_graph: self.scene_graph_for_ui(),
2722 invalidates_ids: false,
2723 new_objects: new_object_ids,
2724 exec_outcome: outcome,
2725 };
2726 Ok((src_delta, scene_graph_delta))
2727 }
2728
2729 fn edit_point(
2730 &mut self,
2731 new_ast: &mut ast::Node<ast::Program>,
2732 sketch: ObjectId,
2733 point: ObjectId,
2734 ctor: PointCtor,
2735 ) -> Result<(), KclError> {
2736 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2738
2739 let sketch_id = sketch;
2741 let sketch_object = self
2742 .scene_graph
2743 .objects
2744 .get(sketch_id.0)
2745 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2746 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2747 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2748 };
2749 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2750 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2751 })?;
2752 let point_id = point;
2754 let point_object = self
2755 .scene_graph
2756 .objects
2757 .get(point_id.0)
2758 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2759 let ObjectKind::Segment {
2760 segment: Segment::Point(point),
2761 } = &point_object.kind
2762 else {
2763 return Err(KclError::refactor(format!(
2764 "Object is not a point segment: {point_object:?}"
2765 )));
2766 };
2767
2768 if let Some(owner_id) = point.owner {
2770 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2771 KclError::refactor(format!(
2772 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2773 ))
2774 })?;
2775 let ObjectKind::Segment { segment } = &owner_object.kind else {
2776 return Err(KclError::refactor(format!(
2777 "Internal: Owner of point is not a segment, but found {}",
2778 owner_object.kind.human_friendly_kind_with_article()
2779 )));
2780 };
2781
2782 if let Segment::Line(line) = segment {
2784 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2785 return Err(KclError::refactor(format!(
2786 "Internal: Owner of point does not have line ctor, but found {}",
2787 line.ctor.human_friendly_kind_with_article()
2788 )));
2789 };
2790 let mut line_ctor = line_ctor.clone();
2791 if line.start == point_id {
2793 line_ctor.start = ctor.position;
2794 } else if line.end == point_id {
2795 line_ctor.end = ctor.position;
2796 } else {
2797 return Err(KclError::refactor(format!(
2798 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2799 )));
2800 }
2801 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2802 }
2803
2804 if let Segment::Arc(arc) = segment {
2806 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2807 return Err(KclError::refactor(format!(
2808 "Internal: Owner of point does not have arc ctor, but found {}",
2809 arc.ctor.human_friendly_kind_with_article()
2810 )));
2811 };
2812 let mut arc_ctor = arc_ctor.clone();
2813 if arc.center == point_id {
2815 arc_ctor.center = ctor.position;
2816 } else if arc.start == point_id {
2817 arc_ctor.start = ctor.position;
2818 } else if arc.end == point_id {
2819 arc_ctor.end = ctor.position;
2820 } else {
2821 return Err(KclError::refactor(format!(
2822 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2823 )));
2824 }
2825 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2826 }
2827
2828 if let Segment::Circle(circle) = segment {
2830 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2831 return Err(KclError::refactor(format!(
2832 "Internal: Owner of point does not have circle ctor, but found {}",
2833 circle.ctor.human_friendly_kind_with_article()
2834 )));
2835 };
2836 let mut circle_ctor = circle_ctor.clone();
2837 if circle.center == point_id {
2838 circle_ctor.center = ctor.position;
2839 } else if circle.start == point_id {
2840 circle_ctor.start = ctor.position;
2841 } else {
2842 return Err(KclError::refactor(format!(
2843 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2844 )));
2845 }
2846 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2847 }
2848
2849 if let Segment::ControlPointSpline(spline) = segment {
2850 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2851 return Err(KclError::refactor(format!(
2852 "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2853 spline.ctor.human_friendly_kind_with_article()
2854 )));
2855 };
2856 let mut spline_ctor = spline_ctor.clone();
2857 let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2858 return Err(KclError::refactor(format!(
2859 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2860 )));
2861 };
2862 spline_ctor.points[control_index] = ctor.position;
2863 return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2864 }
2865
2866 }
2869
2870 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2872 Ok(())
2873 }
2874
2875 fn edit_line(
2876 &mut self,
2877 new_ast: &mut ast::Node<ast::Program>,
2878 sketch: ObjectId,
2879 line: ObjectId,
2880 ctor: LineCtor,
2881 ) -> Result<(), KclError> {
2882 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2884 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2885
2886 let sketch_id = sketch;
2888 let sketch_object = self
2889 .scene_graph
2890 .objects
2891 .get(sketch_id.0)
2892 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2893 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2894 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2895 };
2896 sketch
2897 .segments
2898 .iter()
2899 .find(|o| **o == line)
2900 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2901 let line_id = line;
2903 let line_object = self
2904 .scene_graph
2905 .objects
2906 .get(line_id.0)
2907 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2908 let ObjectKind::Segment { .. } = &line_object.kind else {
2909 let kind = line_object.kind.human_friendly_kind_with_article();
2910 return Err(KclError::refactor(format!(
2911 "This constraint only works on Segments, but you selected {kind}"
2912 )));
2913 };
2914
2915 self.mutate_ast(
2917 new_ast,
2918 line_id,
2919 AstMutateCommand::EditLine {
2920 start: new_start_ast,
2921 end: new_end_ast,
2922 construction: ctor.construction,
2923 },
2924 )?;
2925 Ok(())
2926 }
2927
2928 fn edit_arc(
2929 &mut self,
2930 new_ast: &mut ast::Node<ast::Program>,
2931 sketch: ObjectId,
2932 arc: ObjectId,
2933 ctor: ArcCtor,
2934 ) -> Result<(), KclError> {
2935 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2937 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2938 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2939
2940 let sketch_id = sketch;
2942 let sketch_object = self
2943 .scene_graph
2944 .objects
2945 .get(sketch_id.0)
2946 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2947 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2948 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2949 };
2950 sketch
2951 .segments
2952 .iter()
2953 .find(|o| **o == arc)
2954 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2955 let arc_id = arc;
2957 let arc_object = self
2958 .scene_graph
2959 .objects
2960 .get(arc_id.0)
2961 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2962 let ObjectKind::Segment { .. } = &arc_object.kind else {
2963 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2964 };
2965
2966 self.mutate_ast(
2968 new_ast,
2969 arc_id,
2970 AstMutateCommand::EditArc {
2971 start: new_start_ast,
2972 end: new_end_ast,
2973 center: new_center_ast,
2974 direction: ctor.direction,
2975 construction: ctor.construction,
2976 },
2977 )?;
2978 Ok(())
2979 }
2980
2981 fn edit_circle(
2982 &mut self,
2983 new_ast: &mut ast::Node<ast::Program>,
2984 sketch: ObjectId,
2985 circle: ObjectId,
2986 ctor: CircleCtor,
2987 ) -> Result<(), KclError> {
2988 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2990 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2991
2992 let sketch_id = sketch;
2994 let sketch_object = self
2995 .scene_graph
2996 .objects
2997 .get(sketch_id.0)
2998 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2999 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3000 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3001 };
3002 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3003 KclError::refactor(format!(
3004 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3005 ))
3006 })?;
3007 let circle_id = circle;
3009 let circle_object = self
3010 .scene_graph
3011 .objects
3012 .get(circle_id.0)
3013 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3014 let ObjectKind::Segment { .. } = &circle_object.kind else {
3015 return Err(KclError::refactor(format!(
3016 "Object is not a segment: {circle_object:?}"
3017 )));
3018 };
3019
3020 self.mutate_ast(
3022 new_ast,
3023 circle_id,
3024 AstMutateCommand::EditCircle {
3025 start: new_start_ast,
3026 center: new_center_ast,
3027 construction: ctor.construction,
3028 },
3029 )?;
3030 Ok(())
3031 }
3032
3033 fn edit_control_point_spline(
3034 &mut self,
3035 new_ast: &mut ast::Node<ast::Program>,
3036 sketch: ObjectId,
3037 spline: ObjectId,
3038 ctor: ControlPointSplineCtor,
3039 ) -> Result<(), KclError> {
3040 let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3041
3042 let sketch_object = self
3043 .scene_graph
3044 .objects
3045 .get(sketch.0)
3046 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3047 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3048 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3049 };
3050 sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3051 KclError::refactor(format!(
3052 "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3053 ))
3054 })?;
3055
3056 let spline_object =
3057 self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3058 KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3059 })?;
3060 let ObjectKind::Segment { .. } = &spline_object.kind else {
3061 return Err(KclError::refactor(format!(
3062 "Object is not a segment: {spline_object:?}"
3063 )));
3064 };
3065
3066 self.mutate_ast(
3067 new_ast,
3068 spline,
3069 AstMutateCommand::EditControlPointSpline {
3070 points: points_ast,
3071 construction: ctor.construction,
3072 },
3073 )?;
3074 Ok(())
3075 }
3076
3077 fn delete_segment(
3078 &mut self,
3079 new_ast: &mut ast::Node<ast::Program>,
3080 sketch: ObjectId,
3081 segment_id: ObjectId,
3082 ) -> Result<(), KclError> {
3083 let sketch_id = sketch;
3085 let sketch_object = self
3086 .scene_graph
3087 .objects
3088 .get(sketch_id.0)
3089 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3090 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3091 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3092 };
3093 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3094 KclError::refactor(format!(
3095 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3096 ))
3097 })?;
3098 let segment_object =
3100 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3101 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3102 })?;
3103 let ObjectKind::Segment { .. } = &segment_object.kind else {
3104 return Err(KclError::refactor(format!(
3105 "Object is not a segment, it is {}",
3106 segment_object.kind.human_friendly_kind_with_article()
3107 )));
3108 };
3109
3110 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3112 Ok(())
3113 }
3114
3115 fn delete_constraint(
3116 &mut self,
3117 new_ast: &mut ast::Node<ast::Program>,
3118 sketch: ObjectId,
3119 constraint_id: ObjectId,
3120 ) -> Result<(), KclError> {
3121 let sketch_id = sketch;
3123 let sketch_object = self
3124 .scene_graph
3125 .objects
3126 .get(sketch_id.0)
3127 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3128 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3129 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3130 };
3131 sketch
3132 .constraints
3133 .iter()
3134 .find(|o| **o == constraint_id)
3135 .ok_or_else(|| {
3136 KclError::refactor(format!(
3137 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3138 ))
3139 })?;
3140 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3142 KclError::refactor(format!(
3143 "Constraint not found in scene graph: constraint={constraint_id:?}"
3144 ))
3145 })?;
3146 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3147 return Err(KclError::refactor(format!(
3148 "Object is not a constraint, it is {}",
3149 constraint_object.kind.human_friendly_kind_with_article()
3150 )));
3151 };
3152
3153 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3155 Ok(())
3156 }
3157
3158 fn edit_coincident_constraint(
3159 &mut self,
3160 new_ast: &mut ast::Node<ast::Program>,
3161 constraint_id: ObjectId,
3162 segments: Vec<ConstraintSegment>,
3163 ) -> Result<(), KclError> {
3164 if segments.len() < 2 {
3165 return Err(KclError::refactor(format!(
3166 "Coincident constraint must have at least 2 inputs, got {}",
3167 segments.len()
3168 )));
3169 }
3170
3171 let segment_asts = segments
3172 .iter()
3173 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3174 .collect::<Result<Vec<_>, _>>()?;
3175
3176 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3177 elements: segment_asts,
3178 digest: None,
3179 non_code_meta: Default::default(),
3180 })));
3181
3182 self.mutate_ast(
3183 new_ast,
3184 constraint_id,
3185 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3186 )?;
3187 Ok(())
3188 }
3189
3190 fn edit_horizontal_points_constraint(
3191 &mut self,
3192 new_ast: &mut ast::Node<ast::Program>,
3193 constraint_id: ObjectId,
3194 points: Vec<ConstraintSegment>,
3195 ) -> Result<(), KclError> {
3196 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3197 }
3198
3199 fn edit_vertical_points_constraint(
3200 &mut self,
3201 new_ast: &mut ast::Node<ast::Program>,
3202 constraint_id: ObjectId,
3203 points: Vec<ConstraintSegment>,
3204 ) -> Result<(), KclError> {
3205 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3206 }
3207
3208 fn edit_axis_points_constraint(
3209 &mut self,
3210 new_ast: &mut ast::Node<ast::Program>,
3211 constraint_id: ObjectId,
3212 points: Vec<ConstraintSegment>,
3213 constraint_name: &str,
3214 ) -> Result<(), KclError> {
3215 if points.len() < 2 {
3216 return Err(KclError::refactor(format!(
3217 "{constraint_name} points constraint must have at least 2 points, got {}",
3218 points.len()
3219 )));
3220 }
3221
3222 let point_asts = points
3223 .iter()
3224 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3225 .collect::<Result<Vec<_>, _>>()?;
3226
3227 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3228 elements: point_asts,
3229 digest: None,
3230 non_code_meta: Default::default(),
3231 })));
3232
3233 self.mutate_ast(
3234 new_ast,
3235 constraint_id,
3236 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3237 )?;
3238 Ok(())
3239 }
3240
3241 fn edit_equal_length_constraint(
3243 &mut self,
3244 new_ast: &mut ast::Node<ast::Program>,
3245 constraint_id: ObjectId,
3246 lines: Vec<ObjectId>,
3247 ) -> Result<(), KclError> {
3248 if lines.len() < 2 {
3249 return Err(KclError::refactor(format!(
3250 "Lines equal length constraint must have at least 2 lines, got {}",
3251 lines.len()
3252 )));
3253 }
3254
3255 let line_asts = lines
3256 .iter()
3257 .map(|line_id| {
3258 let line_object = self
3259 .scene_graph
3260 .objects
3261 .get(line_id.0)
3262 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3263 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3264 let kind = line_object.kind.human_friendly_kind_with_article();
3265 return Err(KclError::refactor(format!(
3266 "This constraint only works on Segments, but you selected {kind}"
3267 )));
3268 };
3269 let Segment::Line(_) = line_segment else {
3270 let kind = line_segment.human_friendly_kind_with_article();
3271 return Err(KclError::refactor(format!(
3272 "Only lines can be made equal length, but you selected {kind}"
3273 )));
3274 };
3275
3276 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3277 })
3278 .collect::<Result<Vec<_>, _>>()?;
3279
3280 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3281 elements: line_asts,
3282 digest: None,
3283 non_code_meta: Default::default(),
3284 })));
3285
3286 self.mutate_ast(
3287 new_ast,
3288 constraint_id,
3289 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3290 )?;
3291 Ok(())
3292 }
3293
3294 fn edit_parallel_constraint(
3296 &mut self,
3297 new_ast: &mut ast::Node<ast::Program>,
3298 constraint_id: ObjectId,
3299 lines: Vec<ObjectId>,
3300 ) -> Result<(), KclError> {
3301 if lines.len() < 2 {
3302 return Err(KclError::refactor(format!(
3303 "Parallel constraint must have at least 2 lines, got {}",
3304 lines.len()
3305 )));
3306 }
3307
3308 let line_asts = lines
3309 .iter()
3310 .map(|line_id| {
3311 let line_object = self
3312 .scene_graph
3313 .objects
3314 .get(line_id.0)
3315 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3316 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3317 let kind = line_object.kind.human_friendly_kind_with_article();
3318 return Err(KclError::refactor(format!(
3319 "This constraint only works on Segments, but you selected {kind}"
3320 )));
3321 };
3322 let Segment::Line(_) = line_segment else {
3323 let kind = line_segment.human_friendly_kind_with_article();
3324 return Err(KclError::refactor(format!(
3325 "Only lines can be made parallel, but you selected {kind}"
3326 )));
3327 };
3328
3329 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3330 })
3331 .collect::<Result<Vec<_>, _>>()?;
3332
3333 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3334 elements: line_asts,
3335 digest: None,
3336 non_code_meta: Default::default(),
3337 })));
3338
3339 self.mutate_ast(
3340 new_ast,
3341 constraint_id,
3342 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3343 )?;
3344 Ok(())
3345 }
3346
3347 fn edit_equal_radius_constraint(
3349 &mut self,
3350 new_ast: &mut ast::Node<ast::Program>,
3351 constraint_id: ObjectId,
3352 input: Vec<ObjectId>,
3353 ) -> Result<(), KclError> {
3354 if input.len() < 2 {
3355 return Err(KclError::refactor(format!(
3356 "equalRadius constraint must have at least 2 segments, got {}",
3357 input.len()
3358 )));
3359 }
3360
3361 let input_asts = input
3362 .iter()
3363 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3364 .collect::<Result<Vec<_>, _>>()?;
3365
3366 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3367 elements: input_asts,
3368 digest: None,
3369 non_code_meta: Default::default(),
3370 })));
3371
3372 self.mutate_ast(
3373 new_ast,
3374 constraint_id,
3375 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3376 )?;
3377 Ok(())
3378 }
3379
3380 async fn execute_after_edit(
3381 &mut self,
3382 ctx: &ExecutorContext,
3383 sketch: ObjectId,
3384 sketch_block_ref: AstNodeRef,
3385 new_ast: &mut ast::Node<ast::Program>,
3386 options: ExecuteAfterEditOptions,
3387 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3388 let ExecuteAfterEditOptions {
3389 segment_ids_edited,
3390 edit_kind,
3391 commit_solved_initial_guesses,
3392 } = options;
3393
3394 let new_source = source_from_ast(new_ast);
3396 let new_program = parse_frontend_mutation_source(
3398 &new_source,
3399 "Error parsing KCL source after editing",
3400 "No AST produced after editing",
3401 )?;
3402
3403 let is_delete = edit_kind.is_delete();
3405 let truncated_program = {
3406 let mut truncated_program = new_program.clone();
3407 only_sketch_block(
3408 &mut truncated_program.ast,
3409 &sketch_block_ref,
3410 edit_kind.to_change_kind(),
3411 )
3412 .map_err(KclErrorWithOutputs::no_outputs)?;
3413 truncated_program
3414 };
3415
3416 let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3418 let mock_config = MockConfig {
3419 sketch_block_id: Some(sketch),
3420 freedom_analysis: is_delete,
3421 segment_ids_edited: segment_ids_edited.clone(),
3422 drag_anchors,
3423 ..Default::default()
3424 };
3425 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3426
3427 self.program = new_program;
3429
3430 let outcome = self.update_state_after_exec(outcome, is_delete);
3432
3433 let src_delta = if commit_solved_initial_guesses {
3434 self.commit_var_solutions_to_program(&outcome, "editing")?
3435 } else {
3436 SourceDelta { text: new_source }
3437 };
3438 let scene_graph_delta = SceneGraphDelta {
3439 new_graph: self.scene_graph_for_ui(),
3440 invalidates_ids: is_delete,
3441 new_objects: Vec::new(),
3442 exec_outcome: outcome,
3443 };
3444 Ok((src_delta, scene_graph_delta))
3445 }
3446
3447 async fn execute_after_delete_sketch(
3448 &mut self,
3449 ctx: &ExecutorContext,
3450 new_ast: &mut ast::Node<ast::Program>,
3451 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3452 let new_source = source_from_ast(new_ast);
3454 let new_program = parse_frontend_mutation_source(
3456 &new_source,
3457 "Error parsing KCL source after editing",
3458 "No AST produced after editing",
3459 )?;
3460
3461 self.program = new_program.clone();
3463
3464 let outcome = ctx.run_with_caching(new_program).await?;
3470 let freedom_analysis_ran = true;
3471
3472 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3473
3474 let src_delta = SourceDelta { text: new_source };
3475 let scene_graph_delta = SceneGraphDelta {
3476 new_graph: self.scene_graph_for_ui(),
3477 invalidates_ids: true,
3478 new_objects: Vec::new(),
3479 exec_outcome: outcome,
3480 };
3481 Ok((src_delta, scene_graph_delta))
3482 }
3483
3484 fn point_id_to_ast_reference(
3489 &self,
3490 point_id: ObjectId,
3491 new_ast: &mut ast::Node<ast::Program>,
3492 ) -> Result<ast::Expr, KclError> {
3493 let point_object = self
3494 .scene_graph
3495 .objects
3496 .get(point_id.0)
3497 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3498 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3499 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3500 };
3501 let Segment::Point(point) = point_segment else {
3502 return Err(KclError::refactor(format!(
3503 "Only points are currently supported: {point_object:?}"
3504 )));
3505 };
3506
3507 if let Some(owner_id) = point.owner {
3508 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3509 KclError::refactor(format!(
3510 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3511 ))
3512 })?;
3513 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3514 return Err(KclError::refactor(format!(
3515 "Owner of point is not a segment, but found {}",
3516 owner_object.kind.human_friendly_kind_with_article()
3517 )));
3518 };
3519
3520 match owner_segment {
3521 Segment::Line(line) => {
3522 let property = if line.start == point_id {
3523 LINE_PROPERTY_START
3524 } else if line.end == point_id {
3525 LINE_PROPERTY_END
3526 } else {
3527 return Err(KclError::refactor(format!(
3528 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3529 )));
3530 };
3531 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3532 }
3533 Segment::Arc(arc) => {
3534 let property = if arc.start == point_id {
3535 ARC_PROPERTY_START
3536 } else if arc.end == point_id {
3537 ARC_PROPERTY_END
3538 } else if arc.center == point_id {
3539 ARC_PROPERTY_CENTER
3540 } else {
3541 return Err(KclError::refactor(format!(
3542 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3543 )));
3544 };
3545 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3546 }
3547 Segment::Circle(circle) => {
3548 let property = if circle.start == point_id {
3549 CIRCLE_PROPERTY_START
3550 } else if circle.center == point_id {
3551 CIRCLE_PROPERTY_CENTER
3552 } else {
3553 return Err(KclError::refactor(format!(
3554 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3555 )));
3556 };
3557 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3558 }
3559 Segment::ControlPointSpline(spline) => {
3560 let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3561 return Err(KclError::refactor(format!(
3562 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3563 )));
3564 };
3565 let owner_expr =
3566 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3567 let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3568 Ok(create_index_expression(controls_expr, index))
3569 }
3570 _ => Err(KclError::refactor(format!(
3571 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3572 ))),
3573 }
3574 } else {
3575 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3577 }
3578 }
3579
3580 fn line_id_to_ast_reference(
3581 &self,
3582 line_id: ObjectId,
3583 new_ast: &mut ast::Node<ast::Program>,
3584 ) -> Result<ast::Expr, KclError> {
3585 let line_object = self
3586 .scene_graph
3587 .objects
3588 .get(line_id.0)
3589 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3590 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3591 return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3592 };
3593 let Segment::Line(line) = line_segment else {
3594 return Err(KclError::refactor(format!(
3595 "Only lines are currently supported: {line_object:?}"
3596 )));
3597 };
3598
3599 if let Some(owner_id) = line.owner {
3600 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3601 KclError::refactor(format!(
3602 "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3603 ))
3604 })?;
3605 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3606 return Err(KclError::refactor(format!(
3607 "Owner of line is not a segment, but found {}",
3608 owner_object.kind.human_friendly_kind_with_article()
3609 )));
3610 };
3611
3612 match owner_segment {
3613 Segment::ControlPointSpline(spline) => {
3614 let Some(index) = spline
3615 .controls
3616 .windows(2)
3617 .position(|window| window[0] == line.start && window[1] == line.end)
3618 else {
3619 return Err(KclError::refactor(format!(
3620 "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3621 )));
3622 };
3623 let owner_expr =
3624 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3625 let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3626 Ok(create_index_expression(edges_expr, index))
3627 }
3628 _ => Err(KclError::refactor(format!(
3629 "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3630 ))),
3631 }
3632 } else {
3633 get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3634 }
3635 }
3636
3637 fn coincident_segment_to_ast(
3638 &self,
3639 segment: &ConstraintSegment,
3640 new_ast: &mut ast::Node<ast::Program>,
3641 ) -> Result<ast::Expr, KclError> {
3642 match segment {
3643 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3644 ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3645 }
3646 }
3647
3648 fn segment_id_to_constraint_ast_reference(
3649 &self,
3650 segment_id: ObjectId,
3651 new_ast: &mut ast::Node<ast::Program>,
3652 ) -> Result<ast::Expr, KclError> {
3653 let segment_object = self
3654 .scene_graph
3655 .objects
3656 .get(segment_id.0)
3657 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3658 let ObjectKind::Segment { segment } = &segment_object.kind else {
3659 return Err(KclError::refactor(format!(
3660 "Object is not a segment, it is {}",
3661 segment_object.kind.human_friendly_kind_with_article()
3662 )));
3663 };
3664
3665 match segment {
3666 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3667 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3668 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3669 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3670 Segment::ControlPointSpline(_) => {
3671 get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3672 }
3673 }
3674 }
3675
3676 fn axis_constraint_segment_to_ast(
3677 &self,
3678 segment: &ConstraintSegment,
3679 new_ast: &mut ast::Node<ast::Program>,
3680 ) -> Result<ast::Expr, KclError> {
3681 match segment {
3682 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3683 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3684 }
3685 }
3686
3687 async fn add_coincident(
3688 &mut self,
3689 sketch: ObjectId,
3690 coincident: Coincident,
3691 new_ast: &mut ast::Node<ast::Program>,
3692 ) -> Result<AstNodeRef, KclError> {
3693 let sketch_id = sketch;
3694 for segment in &coincident.segments {
3695 let ConstraintSegment::Segment(segment_id) = segment else {
3696 continue;
3697 };
3698 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3699 continue;
3700 };
3701 if matches!(
3702 segment_object.kind,
3703 ObjectKind::Segment {
3704 segment: Segment::ControlPointSpline(_)
3705 }
3706 ) {
3707 return Err(KclError::refactor(
3708 "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3709 .to_owned(),
3710 ));
3711 }
3712 }
3713 let segment_asts = coincident
3714 .segments
3715 .iter()
3716 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3717 .collect::<Result<Vec<_>, _>>()?;
3718 if segment_asts.len() < 2 {
3719 return Err(KclError::refactor(format!(
3720 "Coincident constraint must have at least 2 inputs, got {}",
3721 segment_asts.len()
3722 )));
3723 }
3724
3725 let coincident_ast = create_coincident_ast(segment_asts);
3727
3728 let (sketch_block_ref, _) = self.mutate_ast(
3730 new_ast,
3731 sketch_id,
3732 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3733 )?;
3734 Ok(sketch_block_ref)
3735 }
3736
3737 async fn add_distance(
3738 &mut self,
3739 sketch: ObjectId,
3740 distance: Distance,
3741 new_ast: &mut ast::Node<ast::Program>,
3742 ) -> Result<AstNodeRef, KclError> {
3743 self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3744 }
3745
3746 fn distance_constraint_ast_parts(
3747 &self,
3748 function_name: &str,
3749 distance: &Distance,
3750 new_ast: &mut ast::Node<ast::Program>,
3751 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3752 let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3753 [pt0, pt1] => [
3754 self.coincident_segment_to_ast(pt0, new_ast)?,
3755 self.coincident_segment_to_ast(pt1, new_ast)?,
3756 ],
3757 _ => {
3758 return Err(KclError::refactor(format!(
3759 "Distance constraint must have exactly 2 segments, got {}",
3760 distance.segments.len()
3761 )));
3762 }
3763 };
3764
3765 let arguments = match &distance.label_position {
3766 Some(label_position) => vec![ast::LabeledArg {
3767 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3768 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3769 }],
3770 None => Default::default(),
3771 };
3772
3773 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3774 callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3775 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3776 ast::ArrayExpression {
3777 elements: vec![segment0_ast, segment1_ast],
3778 digest: None,
3779 non_code_meta: Default::default(),
3780 },
3781 )))),
3782 arguments,
3783 digest: None,
3784 non_code_meta: Default::default(),
3785 })));
3786 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3787 value: ast::LiteralValue::Number {
3788 value: distance.distance.value,
3789 suffix: distance.distance.units,
3790 },
3791 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3792 KclError::refactor(format!(
3793 "Could not format numeric suffix: {:?}",
3794 distance.distance.units
3795 ))
3796 })?,
3797 digest: None,
3798 })));
3799
3800 Ok((call, value))
3801 }
3802
3803 fn add_distance_constraint(
3804 &mut self,
3805 sketch: ObjectId,
3806 function_name: &str,
3807 distance: Distance,
3808 new_ast: &mut ast::Node<ast::Program>,
3809 ) -> Result<AstNodeRef, KclError> {
3810 let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3811 let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3812 left: call,
3813 operator: ast::BinaryOperator::Eq,
3814 right: value,
3815 digest: None,
3816 })));
3817
3818 let (sketch_block_ref, _) = self.mutate_ast(
3819 new_ast,
3820 sketch,
3821 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3822 )?;
3823 Ok(sketch_block_ref)
3824 }
3825
3826 async fn add_angle(
3827 &mut self,
3828 sketch: ObjectId,
3829 angle: Angle,
3830 new_ast: &mut ast::Node<ast::Program>,
3831 ) -> Result<AstNodeRef, KclError> {
3832 let sketch_id = sketch;
3833 let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3834 let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3835 left: angle_call_ast,
3836 operator: ast::BinaryOperator::Eq,
3837 right: angle_value_ast,
3838 digest: None,
3839 })));
3840
3841 let (sketch_block_ref, _) = self.mutate_ast(
3843 new_ast,
3844 sketch_id,
3845 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3846 )?;
3847 Ok(sketch_block_ref)
3848 }
3849
3850 fn angle_constraint_ast_parts(
3851 &self,
3852 angle: &Angle,
3853 new_ast: &mut ast::Node<ast::Program>,
3854 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3855 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3856 return Err(KclError::refactor(format!(
3857 "Angle constraint must have exactly 2 lines, got {}",
3858 angle.lines.len()
3859 )));
3860 };
3861
3862 let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3863 let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3864 let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3865 elements: vec![l0_ast, l1_ast],
3866 digest: None,
3867 non_code_meta: Default::default(),
3868 })));
3869
3870 if angle.inverse == Some(true) && angle.sector.is_none() {
3871 return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3872 }
3873
3874 let uses_angle_dimension = angle.sector.is_some();
3875 let mut arguments = if uses_angle_dimension {
3876 vec![ast::LabeledArg {
3877 label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3878 arg: lines_ast.clone(),
3879 }]
3880 } else {
3881 Default::default()
3882 };
3883
3884 if let Some(sector) = angle.sector {
3885 arguments.push(ast::LabeledArg {
3886 label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3887 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3888 value: ast::LiteralValue::Number {
3889 value: f64::from(sector),
3890 suffix: NumericSuffix::None,
3891 },
3892 raw: sector.to_string(),
3893 digest: None,
3894 }))),
3895 });
3896 }
3897
3898 if angle.inverse == Some(true) {
3899 arguments.push(ast::LabeledArg {
3900 label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3901 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3902 value: ast::LiteralValue::Bool(true),
3903 raw: true.to_string(),
3904 digest: None,
3905 }))),
3906 });
3907 }
3908
3909 if let Some(label_position) = &angle.label_position {
3910 arguments.push(ast::LabeledArg {
3911 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3912 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3913 });
3914 }
3915
3916 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3917 callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3918 ANGLE_DIMENSION_FN
3919 } else {
3920 ANGLE_FN
3921 })),
3922 unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3923 arguments,
3924 digest: None,
3925 non_code_meta: Default::default(),
3926 })));
3927 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3928 value: ast::LiteralValue::Number {
3929 value: angle.angle.value,
3930 suffix: angle.angle.units,
3931 },
3932 raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3933 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3934 digest: None,
3935 })));
3936
3937 Ok((call, value))
3938 }
3939
3940 async fn add_tangent(
3941 &mut self,
3942 sketch: ObjectId,
3943 tangent: Tangent,
3944 new_ast: &mut ast::Node<ast::Program>,
3945 ) -> Result<AstNodeRef, KclError> {
3946 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3947 return Err(KclError::refactor(format!(
3948 "Tangent constraint must have exactly 2 segments, got {}",
3949 tangent.input.len()
3950 )));
3951 };
3952 let sketch_id = sketch;
3953
3954 let seg0_object = self
3955 .scene_graph
3956 .objects
3957 .get(seg0_id.0)
3958 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3959 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3960 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3961 };
3962 let seg0_ast = match seg0_segment {
3963 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3964 self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3965 }
3966 _ => {
3967 return Err(KclError::refactor(format!(
3968 "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3969 )));
3970 }
3971 };
3972
3973 let seg1_object = self
3974 .scene_graph
3975 .objects
3976 .get(seg1_id.0)
3977 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3978 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3979 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3980 };
3981 let seg1_ast = match seg1_segment {
3982 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3983 self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3984 }
3985 _ => {
3986 return Err(KclError::refactor(format!(
3987 "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3988 )));
3989 }
3990 };
3991
3992 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3993 let (sketch_block_ref, _) = self.mutate_ast(
3994 new_ast,
3995 sketch_id,
3996 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3997 )?;
3998 Ok(sketch_block_ref)
3999 }
4000
4001 async fn add_symmetric(
4002 &mut self,
4003 sketch: ObjectId,
4004 symmetric: Symmetric,
4005 new_ast: &mut ast::Node<ast::Program>,
4006 ) -> Result<AstNodeRef, KclError> {
4007 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4008 return Err(KclError::refactor(format!(
4009 "Symmetric constraint must have exactly 2 inputs, got {}",
4010 symmetric.input.len()
4011 )));
4012 };
4013 let sketch_id = sketch;
4014
4015 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4016 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4017 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4018
4019 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4020 let (sketch_block_ref, _) = self.mutate_ast(
4021 new_ast,
4022 sketch_id,
4023 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4024 )?;
4025 Ok(sketch_block_ref)
4026 }
4027
4028 async fn add_midpoint(
4029 &mut self,
4030 sketch: ObjectId,
4031 midpoint: Midpoint,
4032 new_ast: &mut ast::Node<ast::Program>,
4033 ) -> Result<AstNodeRef, KclError> {
4034 let sketch_id = sketch;
4035 let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4036
4037 let segment_object = self
4038 .scene_graph
4039 .objects
4040 .get(midpoint.segment.0)
4041 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4042 let ObjectKind::Segment {
4043 segment: midpoint_segment,
4044 } = &segment_object.kind
4045 else {
4046 return Err(KclError::refactor(format!(
4047 "Object must be a segment, but it was {}",
4048 segment_object.kind.human_friendly_kind_with_article()
4049 )));
4050 };
4051 let segment_ast = match midpoint_segment {
4052 Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4053 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4054 _ => {
4055 return Err(KclError::refactor(format!(
4056 "Midpoint target must be a line or arc segment but it was {}",
4057 midpoint_segment.human_friendly_kind_with_article()
4058 )));
4059 }
4060 };
4061
4062 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4063 let (sketch_block_ref, _) = self.mutate_ast(
4064 new_ast,
4065 sketch_id,
4066 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4067 )?;
4068 Ok(sketch_block_ref)
4069 }
4070
4071 async fn add_equal_radius(
4072 &mut self,
4073 sketch: ObjectId,
4074 equal_radius: EqualRadius,
4075 new_ast: &mut ast::Node<ast::Program>,
4076 ) -> Result<AstNodeRef, KclError> {
4077 if equal_radius.input.len() < 2 {
4078 return Err(KclError::refactor(format!(
4079 "equalRadius constraint must have at least 2 segments, got {}",
4080 equal_radius.input.len()
4081 )));
4082 }
4083
4084 let sketch_id = sketch;
4085 let input_asts = equal_radius
4086 .input
4087 .iter()
4088 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4089 .collect::<Result<Vec<_>, _>>()?;
4090
4091 let equal_radius_ast = create_equal_radius_ast(input_asts);
4092 let (sketch_block_ref, _) = self.mutate_ast(
4093 new_ast,
4094 sketch_id,
4095 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4096 )?;
4097 Ok(sketch_block_ref)
4098 }
4099
4100 async fn add_radius(
4101 &mut self,
4102 sketch: ObjectId,
4103 radius: Radius,
4104 new_ast: &mut ast::Node<ast::Program>,
4105 ) -> Result<AstNodeRef, KclError> {
4106 let params = ArcSizeConstraintParams {
4107 points: vec![radius.arc],
4108 function_name: RADIUS_FN,
4109 value: radius.radius.value,
4110 units: radius.radius.units,
4111 label_position: radius.label_position,
4112 constraint_type_name: "Radius",
4113 };
4114 self.add_arc_size_constraint(sketch, params, new_ast).await
4115 }
4116
4117 async fn add_diameter(
4118 &mut self,
4119 sketch: ObjectId,
4120 diameter: Diameter,
4121 new_ast: &mut ast::Node<ast::Program>,
4122 ) -> Result<AstNodeRef, KclError> {
4123 let params = ArcSizeConstraintParams {
4124 points: vec![diameter.arc],
4125 function_name: DIAMETER_FN,
4126 value: diameter.diameter.value,
4127 units: diameter.diameter.units,
4128 label_position: diameter.label_position,
4129 constraint_type_name: "Diameter",
4130 };
4131 self.add_arc_size_constraint(sketch, params, new_ast).await
4132 }
4133
4134 async fn add_fixed_constraints(
4135 &mut self,
4136 sketch: ObjectId,
4137 points: Vec<FixedPoint>,
4138 new_ast: &mut ast::Node<ast::Program>,
4139 ) -> Result<AstNodeRef, KclError> {
4140 let mut sketch_block_ref = None;
4141
4142 for fixed_point in points {
4143 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4144 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4145 .map_err(|err| KclError::refactor(err.to_string()))?;
4146
4147 let (sketch_ref, _) = self.mutate_ast(
4148 new_ast,
4149 sketch,
4150 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4151 )?;
4152 sketch_block_ref = Some(sketch_ref);
4153 }
4154
4155 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4156 }
4157
4158 async fn add_arc_size_constraint(
4159 &mut self,
4160 sketch: ObjectId,
4161 params: ArcSizeConstraintParams,
4162 new_ast: &mut ast::Node<ast::Program>,
4163 ) -> Result<AstNodeRef, KclError> {
4164 let sketch_id = sketch;
4165
4166 if params.points.len() != 1 {
4168 return Err(KclError::refactor(format!(
4169 "{} constraint must have exactly 1 argument (an arc segment), got {}",
4170 params.constraint_type_name,
4171 params.points.len()
4172 )));
4173 }
4174
4175 let arc_id = params.points[0];
4176 let arc_object = self
4177 .scene_graph
4178 .objects
4179 .get(arc_id.0)
4180 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4181 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4182 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4183 };
4184 let ref_type = match arc_segment {
4185 Segment::Arc(_) => ARC_VARIABLE,
4186 Segment::Circle(_) => CIRCLE_VARIABLE,
4187 _ => {
4188 return Err(KclError::refactor(format!(
4189 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4190 params.constraint_type_name
4191 )));
4192 }
4193 };
4194 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4196 let arguments = match ¶ms.label_position {
4197 Some(label_position) => vec![ast::LabeledArg {
4198 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4199 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4200 }],
4201 None => Default::default(),
4202 };
4203
4204 let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4206 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4207 unlabeled: Some(arc_ast),
4208 arguments,
4209 digest: None,
4210 non_code_meta: Default::default(),
4211 })));
4212 let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4213 left: call_ast,
4214 operator: ast::BinaryOperator::Eq,
4215 right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4216 value: ast::LiteralValue::Number {
4217 value: params.value,
4218 suffix: params.units,
4219 },
4220 raw: format_number_literal(params.value, params.units, None)
4221 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4222 digest: None,
4223 }))),
4224 digest: None,
4225 })));
4226
4227 let (sketch_block_ref, _) = self.mutate_ast(
4229 new_ast,
4230 sketch_id,
4231 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4232 )?;
4233 Ok(sketch_block_ref)
4234 }
4235
4236 async fn add_horizontal_distance(
4237 &mut self,
4238 sketch: ObjectId,
4239 distance: Distance,
4240 new_ast: &mut ast::Node<ast::Program>,
4241 ) -> Result<AstNodeRef, KclError> {
4242 self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4243 }
4244
4245 async fn add_vertical_distance(
4246 &mut self,
4247 sketch: ObjectId,
4248 distance: Distance,
4249 new_ast: &mut ast::Node<ast::Program>,
4250 ) -> Result<AstNodeRef, KclError> {
4251 self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4252 }
4253
4254 async fn add_horizontal(
4255 &mut self,
4256 sketch: ObjectId,
4257 horizontal: Horizontal,
4258 new_ast: &mut ast::Node<ast::Program>,
4259 ) -> Result<AstNodeRef, KclError> {
4260 let sketch_id = sketch;
4261
4262 let first_arg_ast = match horizontal {
4264 Horizontal::Line { line } => {
4265 let line_object = self
4266 .scene_graph
4267 .objects
4268 .get(line.0)
4269 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4270 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4271 let kind = line_object.kind.human_friendly_kind_with_article();
4272 return Err(KclError::refactor(format!(
4273 "This constraint only works on Segments, but you selected {kind}"
4274 )));
4275 };
4276 let Segment::Line(_) = line_segment else {
4277 return Err(KclError::refactor(format!(
4278 "Only lines can be made horizontal, but you selected {}",
4279 line_segment.human_friendly_kind_with_article(),
4280 )));
4281 };
4282 self.line_id_to_ast_reference(line, new_ast)?
4283 }
4284 Horizontal::Points { points } => {
4285 let point_asts = points
4286 .iter()
4287 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4288 .collect::<Result<Vec<_>, _>>()?;
4289 ast::ArrayExpression::new(point_asts).into()
4290 }
4291 };
4292 let horizontal_ast = create_horizontal_ast(first_arg_ast);
4294
4295 let (sketch_block_ref, _) = self.mutate_ast(
4297 new_ast,
4298 sketch_id,
4299 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4300 )?;
4301 Ok(sketch_block_ref)
4302 }
4303
4304 async fn add_lines_equal_length(
4305 &mut self,
4306 sketch: ObjectId,
4307 lines_equal_length: LinesEqualLength,
4308 new_ast: &mut ast::Node<ast::Program>,
4309 ) -> Result<AstNodeRef, KclError> {
4310 if lines_equal_length.lines.len() < 2 {
4311 return Err(KclError::refactor(format!(
4312 "Lines equal length constraint must have at least 2 lines, got {}",
4313 lines_equal_length.lines.len()
4314 )));
4315 };
4316
4317 let sketch_id = sketch;
4318
4319 let line_asts = lines_equal_length
4321 .lines
4322 .iter()
4323 .map(|line_id| {
4324 let line_object = self
4325 .scene_graph
4326 .objects
4327 .get(line_id.0)
4328 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4329 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4330 let kind = line_object.kind.human_friendly_kind_with_article();
4331 return Err(KclError::refactor(format!(
4332 "This constraint only works on Segments, but you selected {kind}"
4333 )));
4334 };
4335 let Segment::Line(_) = line_segment else {
4336 let kind = line_segment.human_friendly_kind_with_article();
4337 return Err(KclError::refactor(format!(
4338 "Only lines can be made equal length, but you selected {kind}"
4339 )));
4340 };
4341
4342 self.line_id_to_ast_reference(*line_id, new_ast)
4343 })
4344 .collect::<Result<Vec<_>, _>>()?;
4345
4346 let equal_length_ast = create_equal_length_ast(line_asts);
4348
4349 let (sketch_block_ref, _) = self.mutate_ast(
4351 new_ast,
4352 sketch_id,
4353 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4354 )?;
4355 Ok(sketch_block_ref)
4356 }
4357
4358 fn equal_radius_segment_id_to_ast_reference(
4359 &mut self,
4360 segment_id: ObjectId,
4361 new_ast: &mut ast::Node<ast::Program>,
4362 ) -> Result<ast::Expr, KclError> {
4363 let segment_object = self
4364 .scene_graph
4365 .objects
4366 .get(segment_id.0)
4367 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4368 let ObjectKind::Segment { segment } = &segment_object.kind else {
4369 return Err(KclError::refactor(format!(
4370 "Object is not a segment, it was {}",
4371 segment_object.kind.human_friendly_kind_with_article()
4372 )));
4373 };
4374
4375 let ref_type = match segment {
4376 Segment::Arc(_) => ARC_VARIABLE,
4377 Segment::Circle(_) => CIRCLE_VARIABLE,
4378 _ => {
4379 return Err(KclError::refactor(format!(
4380 "equalRadius supports only arc/circle segments, got {}",
4381 segment.human_friendly_kind_with_article()
4382 )));
4383 }
4384 };
4385
4386 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4387 }
4388
4389 fn symmetric_input_id_to_ast_reference(
4390 &mut self,
4391 segment_id: ObjectId,
4392 new_ast: &mut ast::Node<ast::Program>,
4393 ) -> Result<ast::Expr, KclError> {
4394 let segment_object = self
4395 .scene_graph
4396 .objects
4397 .get(segment_id.0)
4398 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4399 let ObjectKind::Segment { segment } = &segment_object.kind else {
4400 return Err(KclError::refactor(format!(
4401 "Object is not a segment, it was {}",
4402 segment_object.kind.human_friendly_kind_with_article()
4403 )));
4404 };
4405
4406 match segment {
4407 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4408 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4409 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4410 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4411 Segment::ControlPointSpline(_) => Err(KclError::refactor(
4412 "Symmetric does not yet support control point splines".to_owned(),
4413 )),
4414 }
4415 }
4416
4417 fn symmetric_axis_id_to_ast_reference(
4418 &mut self,
4419 segment_id: ObjectId,
4420 new_ast: &mut ast::Node<ast::Program>,
4421 ) -> Result<ast::Expr, KclError> {
4422 let segment_object = self
4423 .scene_graph
4424 .objects
4425 .get(segment_id.0)
4426 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4427 let ObjectKind::Segment { segment } = &segment_object.kind else {
4428 return Err(KclError::refactor(format!(
4429 "Object is not a segment, it was {}",
4430 segment_object.kind.human_friendly_kind_with_article()
4431 )));
4432 };
4433 match segment {
4434 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4435 _ => Err(KclError::refactor(format!(
4436 "Symmetric axis must be a line, got {}",
4437 segment.human_friendly_kind_with_article()
4438 ))),
4439 }
4440 }
4441
4442 async fn add_parallel(
4443 &mut self,
4444 sketch: ObjectId,
4445 parallel: Parallel,
4446 new_ast: &mut ast::Node<ast::Program>,
4447 ) -> Result<AstNodeRef, KclError> {
4448 if parallel.lines.len() < 2 {
4449 return Err(KclError::refactor(format!(
4450 "Parallel constraint must have at least 2 lines, got {}",
4451 parallel.lines.len()
4452 )));
4453 };
4454
4455 let sketch_id = sketch;
4456
4457 let line_asts = parallel
4458 .lines
4459 .iter()
4460 .map(|line_id| {
4461 let line_object = self
4462 .scene_graph
4463 .objects
4464 .get(line_id.0)
4465 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4466 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4467 let kind = line_object.kind.human_friendly_kind_with_article();
4468 return Err(KclError::refactor(format!(
4469 "This constraint only works on Segments, but you selected {kind}"
4470 )));
4471 };
4472 let Segment::Line(_) = line_segment else {
4473 let kind = line_segment.human_friendly_kind_with_article();
4474 return Err(KclError::refactor(format!(
4475 "Only lines can be made parallel, but you selected {kind}"
4476 )));
4477 };
4478
4479 self.line_id_to_ast_reference(*line_id, new_ast)
4480 })
4481 .collect::<Result<Vec<_>, _>>()?;
4482
4483 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4484 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4485 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4486 ast::ArrayExpression {
4487 elements: line_asts,
4488 digest: None,
4489 non_code_meta: Default::default(),
4490 },
4491 )))),
4492 arguments: Default::default(),
4493 digest: None,
4494 non_code_meta: Default::default(),
4495 })));
4496
4497 let (sketch_block_ref, _) = self.mutate_ast(
4498 new_ast,
4499 sketch_id,
4500 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4501 )?;
4502 Ok(sketch_block_ref)
4503 }
4504
4505 async fn add_perpendicular(
4506 &mut self,
4507 sketch: ObjectId,
4508 perpendicular: Perpendicular,
4509 new_ast: &mut ast::Node<ast::Program>,
4510 ) -> Result<AstNodeRef, KclError> {
4511 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4512 .await
4513 }
4514
4515 async fn add_lines_at_angle_constraint(
4516 &mut self,
4517 sketch: ObjectId,
4518 angle_kind: LinesAtAngleKind,
4519 lines: Vec<ObjectId>,
4520 new_ast: &mut ast::Node<ast::Program>,
4521 ) -> Result<AstNodeRef, KclError> {
4522 let &[line0_id, line1_id] = lines.as_slice() else {
4523 return Err(KclError::refactor(format!(
4524 "{} constraint must have exactly 2 lines, got {}",
4525 angle_kind.to_function_name(),
4526 lines.len()
4527 )));
4528 };
4529
4530 let sketch_id = sketch;
4531
4532 let line0_object = self
4534 .scene_graph
4535 .objects
4536 .get(line0_id.0)
4537 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4538 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4539 let kind = line0_object.kind.human_friendly_kind_with_article();
4540 return Err(KclError::refactor(format!(
4541 "This constraint only works on Segments, but you selected {kind}"
4542 )));
4543 };
4544 let Segment::Line(_) = line0_segment else {
4545 return Err(KclError::refactor(format!(
4546 "Only lines can be made {}, but you selected {}",
4547 angle_kind.to_function_name(),
4548 line0_segment.human_friendly_kind_with_article(),
4549 )));
4550 };
4551 let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4552
4553 let line1_object = self
4554 .scene_graph
4555 .objects
4556 .get(line1_id.0)
4557 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4558 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4559 let kind = line1_object.kind.human_friendly_kind_with_article();
4560 return Err(KclError::refactor(format!(
4561 "This constraint only works on Segments, but you selected {kind}"
4562 )));
4563 };
4564 let Segment::Line(_) = line1_segment else {
4565 return Err(KclError::refactor(format!(
4566 "Only lines can be made {}, but you selected {}",
4567 angle_kind.to_function_name(),
4568 line1_segment.human_friendly_kind_with_article(),
4569 )));
4570 };
4571 let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4572
4573 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4575 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4576 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4577 ast::ArrayExpression {
4578 elements: vec![line0_ast, line1_ast],
4579 digest: None,
4580 non_code_meta: Default::default(),
4581 },
4582 )))),
4583 arguments: Default::default(),
4584 digest: None,
4585 non_code_meta: Default::default(),
4586 })));
4587
4588 let (sketch_block_ref, _) = self.mutate_ast(
4590 new_ast,
4591 sketch_id,
4592 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4593 )?;
4594 Ok(sketch_block_ref)
4595 }
4596
4597 async fn add_vertical(
4598 &mut self,
4599 sketch: ObjectId,
4600 vertical: Vertical,
4601 new_ast: &mut ast::Node<ast::Program>,
4602 ) -> Result<AstNodeRef, KclError> {
4603 let sketch_id = sketch;
4604
4605 let first_arg_ast = match vertical {
4606 Vertical::Line { line } => {
4607 let line_object = self
4609 .scene_graph
4610 .objects
4611 .get(line.0)
4612 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4613 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4614 let kind = line_object.kind.human_friendly_kind_with_article();
4615 return Err(KclError::refactor(format!(
4616 "This constraint only works on Segments, but you selected {kind}"
4617 )));
4618 };
4619 let Segment::Line(_) = line_segment else {
4620 return Err(KclError::refactor(format!(
4621 "Only lines can be made vertical, but you selected {}",
4622 line_segment.human_friendly_kind_with_article()
4623 )));
4624 };
4625 self.line_id_to_ast_reference(line, new_ast)?
4626 }
4627 Vertical::Points { points } => {
4628 let point_asts = points
4629 .iter()
4630 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4631 .collect::<Result<Vec<_>, _>>()?;
4632 ast::ArrayExpression::new(point_asts).into()
4633 }
4634 };
4635 let vertical_ast = create_vertical_ast(first_arg_ast);
4637
4638 let (sketch_block_ref, _) = self.mutate_ast(
4640 new_ast,
4641 sketch_id,
4642 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4643 )?;
4644 Ok(sketch_block_ref)
4645 }
4646
4647 async fn execute_after_add_constraint(
4648 &mut self,
4649 ctx: &ExecutorContext,
4650 sketch_id: ObjectId,
4651 sketch_block_ref: AstNodeRef,
4652 new_ast: &mut ast::Node<ast::Program>,
4653 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4654 let new_source = source_from_ast(new_ast);
4656 let new_program = parse_frontend_mutation_source(
4658 &new_source,
4659 "Error parsing KCL source after adding constraint",
4660 "No AST produced after adding constraint",
4661 )?;
4662 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4663 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4664 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4665 )))
4666 })?;
4667
4668 let mut truncated_program = new_program.clone();
4671 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4672 .map_err(KclErrorWithOutputs::no_outputs)?;
4673
4674 let outcome = ctx
4676 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4677 .await?;
4678
4679 let new_object_ids = {
4680 let constraint_id = outcome
4682 .source_range_to_object
4683 .get(&constraint_node_ref.range)
4684 .copied()
4685 .ok_or_else(|| {
4686 KclErrorWithOutputs::from_error_outcome(
4687 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4688 outcome.clone(),
4689 )
4690 })?;
4691 vec![constraint_id]
4692 };
4693
4694 self.program = new_program;
4697
4698 let outcome = self.update_state_after_exec(outcome, true);
4700
4701 let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4702 let scene_graph_delta = SceneGraphDelta {
4703 new_graph: self.scene_graph_for_ui(),
4704 invalidates_ids: false,
4705 new_objects: new_object_ids,
4706 exec_outcome: outcome,
4707 };
4708 Ok((src_delta, scene_graph_delta))
4709 }
4710
4711 fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4712 let commit_failure = || {
4713 KclErrorWithOutputs::from_error_outcome(
4714 KclError::refactor(format!("Could not update KCL after {operation}.")),
4715 outcome.clone(),
4716 )
4717 };
4718
4719 let default_length_unit = self.default_length_unit();
4720 let mut settled_ast = self.program.ast.clone();
4721 let mut committed_solver_value = false;
4722 for (var_range, node_path, value) in &outcome.var_solutions {
4723 let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4724 return Err(commit_failure());
4725 };
4726 let new_value = match &lookup {
4727 Some(current_literal) => {
4728 if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4729 continue;
4730 }
4731 preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4732 }
4733 None => {
4734 Number {
4738 value: number_value_in_default_length_units(*value, default_length_unit),
4739 units: default_length_unit.into(),
4740 }
4741 }
4742 };
4743 committed_solver_value = true;
4744 let source_ref = SourceRef::Simple {
4745 range: *var_range,
4746 node_path: node_path.clone(),
4747 };
4748 mutate_ast_node_by_source_ref(
4749 &mut settled_ast,
4750 &source_ref,
4751 AstMutateCommand::EditVarInitialValue { value: new_value },
4752 )
4753 .map_err(|_| commit_failure())?;
4754 }
4755
4756 if !committed_solver_value {
4757 return Ok(SourceDelta {
4758 text: self.program.original_file_contents.clone(),
4759 });
4760 }
4761
4762 let settled_source = source_from_ast(&settled_ast);
4763 let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4764 if !errors.is_empty() {
4765 return Err(commit_failure());
4766 }
4767 let Some(settled_program) = settled_program else {
4768 return Err(commit_failure());
4769 };
4770
4771 self.program = settled_program;
4772
4773 Ok(SourceDelta { text: settled_source })
4774 }
4775
4776 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4778 if segment_ids_set.contains(&segment_id) {
4779 return true;
4780 }
4781
4782 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4783 return false;
4784 };
4785 let ObjectKind::Segment { segment } = &segment_object.kind else {
4786 return false;
4787 };
4788 let Segment::Point(point) = segment else {
4789 return false;
4790 };
4791
4792 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4793 }
4794
4795 fn remaining_constraint_segments(
4796 &self,
4797 segments: &[ConstraintSegment],
4798 segment_ids_set: &AhashIndexSet<ObjectId>,
4799 ) -> Vec<ConstraintSegment> {
4800 segments
4801 .iter()
4802 .copied()
4803 .filter(|segment| match segment {
4804 ConstraintSegment::Origin(_) => true,
4805 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4806 })
4807 .collect()
4808 }
4809
4810 fn find_referenced_constraints(
4811 &self,
4812 sketch_id: ObjectId,
4813 segment_ids_set: &AhashIndexSet<ObjectId>,
4814 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4815 let sketch_object = self
4817 .scene_graph
4818 .objects
4819 .get(sketch_id.0)
4820 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4821 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4822 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4823 };
4824 let segment_or_owner_matches = |segment_id: ObjectId| {
4825 if segment_ids_set.contains(&segment_id) {
4826 return true;
4827 }
4828 let segment_object = self.scene_graph.objects.get(segment_id.0);
4829 if let Some(obj) = segment_object
4830 && let ObjectKind::Segment { segment } = &obj.kind
4831 {
4832 match segment {
4833 Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4834 Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4835 _ => false,
4836 }
4837 } else {
4838 false
4839 }
4840 };
4841 let mut constraint_ids_set = AhashIndexSet::default();
4842 for constraint_id in &sketch.constraints {
4843 let constraint_object = self
4844 .scene_graph
4845 .objects
4846 .get(constraint_id.0)
4847 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4848 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4849 return Err(KclError::refactor(format!(
4850 "Object is not a constraint, it is {}",
4851 constraint_object.kind.human_friendly_kind_with_article()
4852 )));
4853 };
4854 let depends_on_segment = match constraint {
4855 Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4856 Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4857 Constraint::Fixed(fixed) => fixed
4858 .points
4859 .iter()
4860 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4861 Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4862 Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4863 Constraint::EqualRadius(equal_radius) => {
4864 equal_radius.input.iter().copied().any(segment_or_owner_matches)
4865 }
4866 Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4867 Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4868 Constraint::Horizontal(h) => match h {
4869 Horizontal::Line { line } => segment_or_owner_matches(*line),
4870 Horizontal::Points { points } => points.iter().any(|point| match point {
4871 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4872 ConstraintSegment::Origin(_) => false,
4873 }),
4874 },
4875 Constraint::Vertical(v) => match v {
4876 Vertical::Line { line } => segment_or_owner_matches(*line),
4877 Vertical::Points { points } => points.iter().any(|point| match point {
4878 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4879 ConstraintSegment::Origin(_) => false,
4880 }),
4881 },
4882 Constraint::LinesEqualLength(lines_equal_length) => {
4883 lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4884 }
4885 Constraint::Midpoint(midpoint) => {
4886 segment_or_owner_matches(midpoint.segment)
4887 || matches!(
4888 midpoint.point,
4889 ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4890 )
4891 }
4892 Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4893 Constraint::Perpendicular(perpendicular) => {
4894 perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4895 }
4896 Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4897 Constraint::Symmetric(symmetric) => {
4898 segment_or_owner_matches(symmetric.axis)
4899 || symmetric.input.iter().copied().any(segment_or_owner_matches)
4900 }
4901 Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4902 };
4903 if depends_on_segment {
4904 constraint_ids_set.insert(*constraint_id);
4905 }
4906 }
4907 Ok(constraint_ids_set)
4908 }
4909
4910 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4911 let mut outcome = outcome;
4912 self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4913 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4914
4915 if freedom_analysis_ran {
4916 self.point_freedom_cache.clear();
4919 for new_obj in &new_objects {
4920 if let ObjectKind::Segment {
4921 segment: crate::front::Segment::Point(point),
4922 } = &new_obj.kind
4923 {
4924 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4925 }
4926 }
4927 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4928 self.scene_graph.objects = new_objects;
4930 } else {
4931 for old_obj in &self.scene_graph.objects {
4934 if let ObjectKind::Segment {
4935 segment: crate::front::Segment::Point(point),
4936 } = &old_obj.kind
4937 {
4938 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4939 }
4940 }
4941
4942 let mut updated_objects = Vec::with_capacity(new_objects.len());
4944 for new_obj in new_objects {
4945 let mut obj = new_obj;
4946 if let ObjectKind::Segment {
4947 segment: crate::front::Segment::Point(point),
4948 } = &mut obj.kind
4949 {
4950 let new_freedom = point.freedom;
4951 match new_freedom {
4957 Freedom::Free => {
4958 match self.point_freedom_cache.get(&obj.id).copied() {
4959 Some(Freedom::Conflict) => {
4960 }
4963 Some(Freedom::Fixed) => {
4964 point.freedom = Freedom::Fixed;
4966 }
4967 Some(Freedom::Free) => {
4968 }
4970 None => {
4971 }
4973 }
4974 }
4975 Freedom::Fixed => {
4976 }
4978 Freedom::Conflict => {
4979 }
4981 }
4982 self.point_freedom_cache.insert(obj.id, point.freedom);
4984 }
4985 updated_objects.push(obj);
4986 }
4987
4988 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4989 self.scene_graph.objects = updated_objects;
4990 }
4991 outcome
4992 }
4993
4994 fn mutate_ast(
4995 &mut self,
4996 ast: &mut ast::Node<ast::Program>,
4997 object_id: ObjectId,
4998 command: AstMutateCommand,
4999 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5000 let sketch_object = self
5001 .scene_graph
5002 .objects
5003 .get(object_id.0)
5004 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5005 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5006 }
5007
5008 fn mutate_constraint_label_position(
5009 &mut self,
5010 ast: &mut ast::Node<ast::Program>,
5011 constraint_id: ObjectId,
5012 label_position: Point2d<Number>,
5013 ) -> Result<(), KclError> {
5014 let object = self
5015 .scene_graph
5016 .objects
5017 .get(constraint_id.0)
5018 .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5019 if !matches!(
5020 &object.kind,
5021 ObjectKind::Constraint {
5022 constraint: Constraint::Distance(_)
5023 | Constraint::HorizontalDistance(_)
5024 | Constraint::VerticalDistance(_)
5025 | Constraint::Radius(_)
5026 | Constraint::Diameter(_)
5027 | Constraint::Angle(_),
5028 }
5029 ) {
5030 return Err(KclError::refactor(format!(
5031 "Object does not support labelPosition: {constraint_id:?}"
5032 )));
5033 }
5034
5035 let label_position = to_ast_point2d_number(&label_position)
5036 .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5037 self.mutate_ast(
5038 ast,
5039 constraint_id,
5040 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5041 )?;
5042 Ok(())
5043 }
5044}
5045
5046fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5047 let sketch_object = scene_graph
5049 .objects
5050 .get(sketch_id.0)
5051 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5052 let ObjectKind::Sketch(_) = &sketch_object.kind else {
5053 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5054 };
5055 expect_single_node_ref(sketch_object)
5056}
5057
5058fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5059 match &object.source {
5060 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5061 range: *range,
5062 node_path: node_path.clone(),
5063 }),
5064 SourceRef::BackTrace { ranges } => {
5065 let [range] = ranges.as_slice() else {
5066 return Err(KclError::refactor(format!(
5067 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5068 ranges.len()
5069 )));
5070 };
5071 Ok(AstNodeRef {
5072 range: range.0,
5073 node_path: range.1.clone(),
5074 })
5075 }
5076 }
5077}
5078
5079fn only_sketch_block_from_range(
5082 ast: &mut ast::Node<ast::Program>,
5083 sketch_block_range: SourceRange,
5084 edit_kind: ChangeKind,
5085) -> Result<(), KclError> {
5086 let r1 = sketch_block_range;
5087 let matches_range = |r2: SourceRange| -> bool {
5088 match edit_kind {
5091 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5092 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5094 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5095 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5097 }
5098 };
5099 let mut found = false;
5100 for item in ast.body.iter_mut() {
5101 match item {
5102 ast::BodyItem::ImportStatement(_) => {}
5103 ast::BodyItem::ExpressionStatement(node) => {
5104 if matches_range(SourceRange::from(&*node))
5105 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5106 {
5107 sketch_block.is_being_edited = true;
5108 found = true;
5109 break;
5110 }
5111 }
5112 ast::BodyItem::VariableDeclaration(node) => {
5113 if matches_range(SourceRange::from(&node.declaration.init))
5114 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5115 {
5116 sketch_block.is_being_edited = true;
5117 found = true;
5118 break;
5119 }
5120 }
5121 ast::BodyItem::TypeDeclaration(_) => {}
5122 ast::BodyItem::ReturnStatement(node) => {
5123 if matches_range(SourceRange::from(&node.argument))
5124 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5125 {
5126 sketch_block.is_being_edited = true;
5127 found = true;
5128 break;
5129 }
5130 }
5131 }
5132 }
5133 if !found {
5134 return Err(KclError::refactor(format!(
5135 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5136 )));
5137 }
5138
5139 Ok(())
5140}
5141
5142fn only_sketch_block(
5143 ast: &mut ast::Node<ast::Program>,
5144 sketch_block_ref: &AstNodeRef,
5145 edit_kind: ChangeKind,
5146) -> Result<(), KclError> {
5147 let Some(target_node_path) = &sketch_block_ref.node_path else {
5148 #[cfg(target_arch = "wasm32")]
5149 web_sys::console::warn_1(
5150 &format!(
5151 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5152 sketch_block_ref
5153 )
5154 .into(),
5155 );
5156 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5157 };
5158 struct MarkSketchBlockBeingEdited<'a> {
5159 target_node_path: &'a ast::NodePath,
5160 }
5161
5162 impl Visitor for MarkSketchBlockBeingEdited<'_> {
5163 type Break = ();
5164 type Continue = ();
5165
5166 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5167 if let NodeMut::SketchBlock(sketch_block) = node
5168 && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5169 {
5170 sketch_block.is_being_edited = true;
5171 return TraversalReturn::new_break(());
5172 }
5173 TraversalReturn::new_continue(())
5174 }
5175
5176 fn finish(&mut self, _node: NodeMut<'_>) {}
5177 }
5178
5179 let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5180 let found = dfs_mut(ast, &mut marker).is_break();
5181 if !found {
5182 return Err(KclError::refactor(format!(
5183 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5184 )));
5185 }
5186
5187 Ok(())
5188}
5189
5190fn sketch_on_ast_expr(
5191 ast: &mut ast::Node<ast::Program>,
5192 scene_graph: &SceneGraph,
5193 solid_references: &HashMap<Uuid, SolidAstReference>,
5194 on: &Plane,
5195) -> Result<ast::Expr, KclError> {
5196 match on {
5197 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5198 Plane::Object(object_id) => {
5199 let on_object = scene_graph
5200 .objects
5201 .get(object_id.0)
5202 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5203 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5204 return Ok(face_expr);
5205 }
5206 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5207 }
5208 Plane::PrimitiveFace(face) => {
5209 let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5210 KclError::refactor(format!(
5211 "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5212 face.solid_id
5213 ))
5214 })?;
5215 let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5216 Ok(create_face_of_ast(solid_expr, face_id_expr))
5217 }
5218 }
5219}
5220
5221fn solid_references_from_variables(
5222 ast: &ast::Node<ast::Program>,
5223 variables: &IndexMap<String, KclValueView>,
5224) -> HashMap<Uuid, SolidAstReference> {
5225 let mut references = HashMap::new();
5226
5227 for item in &ast.body {
5230 let ast::BodyItem::VariableDeclaration(declaration) = item else {
5231 continue;
5232 };
5233 let name = &declaration.declaration.id.name;
5234 let Some(value) = variables.get(name) else {
5235 continue;
5236 };
5237
5238 match value {
5239 KclValueView::Solid { value } => {
5240 references.insert(
5241 value.id,
5242 SolidAstReference {
5243 variable_name: name.clone(),
5244 output_index: None,
5245 },
5246 );
5247 }
5248 KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5249 for (output_index, entry) in value.iter().enumerate() {
5250 if let KclValueView::Solid { value } = entry {
5251 references.insert(
5252 value.id,
5253 SolidAstReference {
5254 variable_name: name.clone(),
5255 output_index: Some(output_index),
5256 },
5257 );
5258 }
5259 }
5260 }
5261 _ => {}
5262 }
5263 }
5264
5265 references
5266}
5267
5268fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5269 let reference = solid_references.get(&solid_id)?;
5270 let solid_expr = ast_name_expr(reference.variable_name.clone());
5271 Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5272}
5273
5274fn sketch_face_of_scene_object_ast_expr(
5275 ast: &mut ast::Node<ast::Program>,
5276 on_object: &crate::front::Object,
5277) -> Result<Option<ast::Expr>, KclError> {
5278 match &on_object.kind {
5279 ObjectKind::Wall(wall) => {
5280 let solid_ref = get_or_insert_ast_reference(
5281 ast,
5282 &source_ref_from_source_ref_range(&wall.source.solid),
5283 "solid",
5284 None,
5285 )?;
5286 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5287 return Err(KclError::refactor(format!(
5288 "Could not resolve solid reference for selected wall: artifact_id={:?}",
5289 on_object.artifact_id
5290 )));
5291 };
5292 let solid_expr = indexed_solid_expr_for_sweep_output(
5293 ast_name_expr(solid_name_expr.name.name.clone()),
5294 wall.solid_output_index,
5295 );
5296 let sweep_ref = get_or_insert_ast_reference(
5297 ast,
5298 &source_ref_from_source_ref_range(&wall.source.sweep),
5299 "solid",
5300 None,
5301 )?;
5302 let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5303 return Err(KclError::refactor(format!(
5304 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5305 on_object.artifact_id
5306 )));
5307 };
5308 let sweep_name = sweep_name_expr.name.name.clone();
5309 let segment_ref = get_or_insert_ast_reference(
5310 ast,
5311 &source_ref_from_source_ref_range(&wall.source.segment),
5312 LINE_VARIABLE,
5313 None,
5314 )?;
5315
5316 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5317 wall.source
5318 .path
5319 .as_ref()
5320 .and_then(|path_source| region_name_from_path_source(ast, path_source))
5321 }) {
5322 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5323 return Err(KclError::refactor(format!(
5324 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5325 on_object.artifact_id
5326 )));
5327 };
5328 create_member_expression(
5329 create_member_expression(ast_name_expr(region_name), "tags"),
5330 &segment_name_expr.name.name,
5331 )
5332 } else {
5333 segment_ref
5334 };
5335
5336 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5337 }
5338 ObjectKind::Cap(cap) => {
5339 let solid_ref =
5340 get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5341 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5342 return Err(KclError::refactor(format!(
5343 "Could not resolve solid reference for selected cap: artifact_id={:?}",
5344 on_object.artifact_id
5345 )));
5346 };
5347 let solid_expr = indexed_solid_expr_for_sweep_output(
5348 ast_name_expr(solid_name_expr.name.name.clone()),
5349 cap.solid_output_index,
5350 );
5351 let face_expr = match cap.kind {
5353 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5354 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5355 };
5356
5357 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5358 }
5359 _ => Ok(None),
5360 }
5361}
5362
5363fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5364 match solid_output_index {
5365 Some(output_index) => create_index_expression(solid_expr, output_index),
5366 None => solid_expr,
5367 }
5368}
5369
5370fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5371 SourceRef::Simple {
5372 range: source.range,
5373 node_path: source.node_path.clone(),
5374 }
5375}
5376
5377fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5378 let source_ref = source_ref_from_source_ref_range(path_source);
5379 let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5380 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5381 return None;
5382 };
5383 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5384 return None;
5385 };
5386 if region_call.callee.name.name != "region" {
5387 return None;
5388 }
5389 Some(candidate)
5390}
5391
5392fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5393 let mut current_id = source_id;
5394 let mut current_composite = None;
5395 let mut visited = HashSet::new();
5396
5397 while visited.insert(current_id) {
5398 let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5399
5400 let Some(composite_id) = next_composite_id else {
5401 break;
5402 };
5403 let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5404 break;
5405 };
5406
5407 current_id = composite.id;
5408 current_composite = Some(composite);
5409
5410 if !composite.consumed {
5411 break;
5412 }
5413 }
5414
5415 current_composite.map(|composite| &composite.code_ref)
5416}
5417
5418fn downstream_composite_id_for_solid_source(
5419 artifact_graph: &ArtifactGraph,
5420 source_id: ArtifactId,
5421) -> Option<ArtifactId> {
5422 if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5424 && let Some(composite_id) = path.composite_solid_id
5425 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5426 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5427 {
5428 return Some(composite_id);
5429 }
5430
5431 for artifact in artifact_graph.values() {
5433 if let Artifact::Path(path) = artifact
5434 && path.sweep_id == Some(source_id)
5435 && let Some(composite_id) = path.composite_solid_id
5436 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5437 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5438 {
5439 return Some(composite_id);
5440 }
5441 }
5442
5443 artifact_graph.values().find_map(|artifact| {
5445 let Artifact::CompositeSolid(composite) = artifact else {
5446 return None;
5447 };
5448 composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5449 })
5450}
5451
5452fn composite_contains_path_input(
5453 solid_ids: &[ArtifactId],
5454 tool_ids: &[ArtifactId],
5455 path_id: ArtifactId,
5456 solid2d_id: Option<ArtifactId>,
5457) -> bool {
5458 composite_contains_input(solid_ids, tool_ids, path_id)
5459 || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5460}
5461
5462fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5463 solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5464}
5465
5466fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5467 let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5468 SourceRefRange {
5469 range: code_ref.range,
5470 node_path,
5471 }
5472}
5473
5474fn solid_output_index_for_sweep(
5475 artifact_graph: &ArtifactGraph,
5476 sweep_id: ArtifactId,
5477 sweep_code_ref: &CodeRef,
5478) -> Option<usize> {
5479 if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5482 return None;
5483 }
5484
5485 let sibling_sweeps = artifact_graph
5486 .values()
5487 .filter_map(|artifact| match artifact {
5488 Artifact::Sweep(sweep)
5489 if sweep.code_ref.range == sweep_code_ref.range
5490 && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5491 {
5492 Some(sweep)
5493 }
5494 _ => None,
5495 })
5496 .collect::<Vec<_>>();
5497
5498 if sibling_sweeps.len() <= 1 {
5499 return None;
5500 }
5501
5502 sibling_sweeps
5503 .iter()
5504 .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5505}
5506
5507fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5508 let mut existing_artifact_ids = scene_objects
5509 .iter()
5510 .map(|object| object.artifact_id)
5511 .collect::<HashSet<_>>();
5512
5513 for artifact in artifact_graph.values() {
5514 match artifact {
5515 Artifact::Wall(wall) => {
5516 if existing_artifact_ids.contains(&wall.id) {
5517 continue;
5518 }
5519
5520 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5521 Artifact::Segment(segment) => Some(segment),
5522 _ => None,
5523 }) else {
5524 continue;
5525 };
5526 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5527 Artifact::Sweep(sweep) => Some(sweep),
5528 _ => None,
5529 }) else {
5530 continue;
5531 };
5532 let source_segment = segment
5533 .original_seg_id
5534 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5535 .and_then(|artifact| match artifact {
5536 Artifact::Segment(segment) => Some(segment),
5537 _ => None,
5538 })
5539 .unwrap_or(segment);
5540 let solid_code_ref =
5541 downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5542 let path_code_ref = artifact_graph
5543 .get(&segment.path_id)
5544 .or_else(|| artifact_graph.get(&sweep.path_id))
5545 .and_then(|artifact| match artifact {
5546 Artifact::Path(path) => Some(&path.code_ref),
5547 _ => None,
5548 });
5549 let source = WallSource {
5550 solid: code_ref_source_ref_range(solid_code_ref),
5551 sweep: code_ref_source_ref_range(&sweep.code_ref),
5552 path: path_code_ref.map(code_ref_source_ref_range),
5553 segment: code_ref_source_ref_range(&source_segment.code_ref),
5554 };
5555 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5556 && solid_code_ref.node_path == sweep.code_ref.node_path)
5557 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5558 .flatten();
5559 let object_source = source_ref_from_source_ref_range(&source.solid);
5560 let id = ObjectId(scene_objects.len());
5561 scene_objects.push(crate::front::Object {
5562 id,
5563 kind: ObjectKind::Wall(crate::frontend::api::Wall {
5564 id,
5565 source,
5566 solid_output_index,
5567 }),
5568 label: Default::default(),
5569 comments: Default::default(),
5570 artifact_id: wall.id,
5571 source: object_source,
5572 });
5573 existing_artifact_ids.insert(wall.id);
5574 }
5575 Artifact::Cap(cap) => {
5576 if existing_artifact_ids.contains(&cap.id) {
5577 continue;
5578 }
5579
5580 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5581 Artifact::Sweep(sweep) => Some(sweep),
5582 _ => None,
5583 }) else {
5584 continue;
5585 };
5586 let id = ObjectId(scene_objects.len());
5587 let kind = match cap.sub_type {
5588 CapSubType::Start => crate::frontend::api::CapKind::Start,
5589 CapSubType::End => crate::frontend::api::CapKind::End,
5590 };
5591 let solid_code_ref =
5592 downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5593 let source = CapSource {
5594 solid: code_ref_source_ref_range(solid_code_ref),
5595 sweep: code_ref_source_ref_range(&sweep.code_ref),
5596 };
5597 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5598 && solid_code_ref.node_path == sweep.code_ref.node_path)
5599 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5600 .flatten();
5601 let object_source = source_ref_from_source_ref_range(&source.solid);
5602 scene_objects.push(crate::front::Object {
5603 id,
5604 kind: ObjectKind::Cap(crate::frontend::api::Cap {
5605 id,
5606 kind,
5607 source,
5608 solid_output_index,
5609 }),
5610 label: Default::default(),
5611 comments: Default::default(),
5612 artifact_id: cap.id,
5613 source: object_source,
5614 });
5615 existing_artifact_ids.insert(cap.id);
5616 }
5617 _ => {}
5618 }
5619 }
5620}
5621
5622fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5623 use crate::engine::PlaneName;
5624
5625 match name {
5626 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5627 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5628 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5629 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5630 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5631 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5632 }
5633}
5634
5635fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5636 ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5637 ast::UnaryOperator::Neg,
5638 ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5639 )))
5640}
5641
5642fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5643 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5644 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5645 unlabeled: Some(solid_expr),
5646 arguments: vec![ast::LabeledArg {
5647 label: Some(ast::Identifier::new("face")),
5648 arg: face_expr,
5649 }],
5650 digest: None,
5651 non_code_meta: Default::default(),
5652 })))
5653}
5654
5655fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5656 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5657 callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5658 unlabeled: Some(solid_expr),
5659 arguments: vec![ast::LabeledArg {
5660 label: Some(ast::Identifier::new("index")),
5661 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5662 ast::NumericLiteral {
5663 value: index as f64,
5664 suffix: NumericSuffix::None,
5665 raw: index.to_string(),
5666 digest: None,
5667 },
5668 )))),
5669 }],
5670 digest: None,
5671 non_code_meta: Default::default(),
5672 })))
5673}
5674
5675fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5676 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5677 return None;
5678 };
5679 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5680 return None;
5681 };
5682 if !matches!(
5683 sweep_call.callee.name.name.as_str(),
5684 "extrude" | "revolve" | "sweep" | "loft"
5685 ) {
5686 return None;
5687 }
5688 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5689 return None;
5690 };
5691 let candidate = region_name_expr.name.name.clone();
5692 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5693 return None;
5694 };
5695 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5696 return None;
5697 };
5698 if region_call.callee.name.name != "region" {
5699 return None;
5700 }
5701 Some(candidate)
5702}
5703
5704fn get_or_insert_ast_reference(
5711 ast: &mut ast::Node<ast::Program>,
5712 source_ref: &SourceRef,
5713 prefix: &str,
5714 property: Option<&str>,
5715) -> Result<ast::Expr, KclError> {
5716 let command = AstMutateCommand::AddVariableDeclaration {
5717 prefix: prefix.to_owned(),
5718 };
5719 let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5720 Ok((_, ret)) => ret,
5721 Err(err) => {
5722 if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5723 AstMutateCommandReturn::Name(var_name)
5724 } else {
5725 return Err(err);
5726 }
5727 }
5728 };
5729 let AstMutateCommandReturn::Name(var_name) = ret else {
5730 return Err(KclError::refactor(
5731 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5732 ));
5733 };
5734 let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5735 let Some(property) = property else {
5736 return Ok(var_expr);
5738 };
5739
5740 Ok(create_member_expression(var_expr, property))
5741}
5742
5743fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5744 let source_range = match source_ref {
5745 SourceRef::Simple { range, .. } => *range,
5746 SourceRef::BackTrace { ranges } => {
5747 let [range] = ranges.as_slice() else {
5748 return None;
5749 };
5750 range.0
5751 }
5752 };
5753 ast.body.iter().find_map(|item| {
5754 let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5755 return None;
5756 };
5757 let init_range = SourceRange::from(&var_decl.declaration.init);
5758 let source_is_inside_init = init_range.module_id() == source_range.module_id()
5759 && init_range.start() <= source_range.start()
5760 && source_range.end() <= init_range.end();
5761 if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5762 && init_range != source_range
5763 && source_is_inside_init
5764 {
5765 return None;
5766 }
5767 source_is_inside_init.then(|| var_decl.name().to_owned())
5768 })
5769}
5770
5771fn mutate_ast_node_by_source_ref(
5772 ast: &mut ast::Node<ast::Program>,
5773 source_ref: &SourceRef,
5774 command: AstMutateCommand,
5775) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5776 let (source_range, node_path) = match source_ref {
5777 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5778 SourceRef::BackTrace { ranges } => {
5779 let [range] = ranges.as_slice() else {
5780 return Err(KclError::refactor(format!(
5781 "Expected single source ref, got {}; ranges={ranges:#?}",
5782 ranges.len(),
5783 )));
5784 };
5785 (range.0, range.1.clone())
5786 }
5787 };
5788 let mut context = AstMutateContext {
5789 source_range,
5790 node_path,
5791 command,
5792 defined_names_stack: Default::default(),
5793 };
5794 let control = dfs_mut(ast, &mut context);
5795 match control {
5796 ControlFlow::Continue(_) => Err(KclError::refactor(
5797 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5798 )),
5799 ControlFlow::Break(break_value) => break_value,
5800 }
5801}
5802
5803#[derive(Debug)]
5804struct AstMutateContext {
5805 source_range: SourceRange,
5806 node_path: Option<ast::NodePath>,
5807 command: AstMutateCommand,
5808 defined_names_stack: Vec<HashSet<String>>,
5809}
5810
5811#[derive(Debug)]
5812#[allow(clippy::large_enum_variant)]
5813enum AstMutateCommand {
5814 AddSketchBlockExprStmt {
5816 expr: ast::Expr,
5817 },
5818 AddSketchBlockVarDecl {
5820 prefix: String,
5821 expr: ast::Expr,
5822 },
5823 AddVariableDeclaration {
5824 prefix: String,
5825 },
5826 EditPoint {
5827 at: ast::Expr,
5828 },
5829 EditLine {
5830 start: ast::Expr,
5831 end: ast::Expr,
5832 construction: Option<bool>,
5833 },
5834 EditArc {
5835 start: ast::Expr,
5836 end: ast::Expr,
5837 center: ast::Expr,
5838 direction: Option<ArcDirection>,
5839 construction: Option<bool>,
5840 },
5841 EditCircle {
5842 start: ast::Expr,
5843 center: ast::Expr,
5844 construction: Option<bool>,
5845 },
5846 EditControlPointSpline {
5847 points: ast::Expr,
5848 construction: Option<bool>,
5849 },
5850 EditConstraintValue {
5851 value: ast::BinaryPart,
5852 },
5853 EditAngleConstraint {
5854 call: ast::BinaryPart,
5855 value: ast::BinaryPart,
5856 },
5857 EditDistanceConstraint {
5858 call: ast::BinaryPart,
5859 value: ast::BinaryPart,
5860 },
5861 EditDistanceConstraintLabelPosition {
5862 label_position: ast::Expr,
5863 },
5864 EditCallUnlabeled {
5865 arg: ast::Expr,
5866 },
5867 EditVarInitialValue {
5868 value: Number,
5869 },
5870 DeleteNode,
5871}
5872
5873impl AstMutateCommand {
5874 fn needs_defined_names_stack(&self) -> bool {
5875 matches!(
5876 self,
5877 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5878 )
5879 }
5880}
5881
5882#[derive(Debug)]
5883enum AstMutateCommandReturn {
5884 None,
5885 Name(String),
5886}
5887
5888#[derive(Debug, Clone)]
5889struct AstNodeRef {
5890 range: SourceRange,
5891 node_path: Option<ast::NodePath>,
5892}
5893
5894impl<T> From<&ast::Node<T>> for AstNodeRef {
5895 fn from(value: &ast::Node<T>) -> Self {
5896 AstNodeRef {
5897 range: value.into(),
5898 node_path: value.node_path.clone(),
5899 }
5900 }
5901}
5902
5903impl From<&ast::BodyItem> for AstNodeRef {
5904 fn from(value: &ast::BodyItem) -> Self {
5905 match value {
5906 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5907 range: node.into(),
5908 node_path: node.node_path.clone(),
5909 },
5910 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5911 range: node.into(),
5912 node_path: node.node_path.clone(),
5913 },
5914 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5915 range: node.into(),
5916 node_path: node.node_path.clone(),
5917 },
5918 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5919 range: node.into(),
5920 node_path: node.node_path.clone(),
5921 },
5922 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5923 range: node.into(),
5924 node_path: node.node_path.clone(),
5925 },
5926 }
5927 }
5928}
5929
5930impl From<&ast::Expr> for AstNodeRef {
5931 fn from(value: &ast::Expr) -> Self {
5932 AstNodeRef {
5933 range: SourceRange::from(value),
5934 node_path: value.node_path().cloned(),
5935 }
5936 }
5937}
5938
5939impl From<&AstMutateContext> for AstNodeRef {
5940 fn from(value: &AstMutateContext) -> Self {
5941 AstNodeRef {
5942 range: value.source_range,
5943 node_path: value.node_path.clone(),
5944 }
5945 }
5946}
5947
5948impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5949 type Error = crate::walk::AstNodeError;
5950
5951 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5952 Ok(AstNodeRef {
5953 range: SourceRange::try_from(value)?,
5954 node_path: value.try_into()?,
5955 })
5956 }
5957}
5958
5959impl From<AstNodeRef> for SourceRange {
5960 fn from(value: AstNodeRef) -> Self {
5961 value.range
5962 }
5963}
5964
5965impl Visitor for AstMutateContext {
5966 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5967 type Continue = ();
5968
5969 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5970 filter_and_process(self, node)
5971 }
5972
5973 fn finish(&mut self, node: NodeMut<'_>) {
5974 match &node {
5975 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5976 self.defined_names_stack.pop();
5977 }
5978 _ => {}
5979 }
5980 }
5981}
5982
5983fn filter_and_process(
5984 ctx: &mut AstMutateContext,
5985 node: NodeMut,
5986) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5987 let Ok(node_range) = SourceRange::try_from(&node) else {
5988 return TraversalReturn::new_continue(());
5990 };
5991 if let NodeMut::VariableDeclaration(var_decl) = &node {
5996 let expr_range = SourceRange::from(&var_decl.declaration.init);
5997 let expr_node_path = var_decl.declaration.init.node_path();
5998 if source_ref_matches(ctx, expr_range, expr_node_path) {
5999 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6000 return TraversalReturn::new_break(Ok((
6003 AstNodeRef::from(&**var_decl),
6004 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6005 )));
6006 }
6007 if let AstMutateCommand::DeleteNode = &ctx.command {
6008 return TraversalReturn {
6011 mutate_body_item: MutateBodyItem::Delete,
6012 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6013 };
6014 }
6015 }
6016 }
6017 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6020 let expr_range = SourceRange::from(&expr_stmt.expression);
6021 let expr_node_path = expr_stmt.expression.node_path();
6022 if source_ref_matches(ctx, expr_range, expr_node_path) {
6023 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6024 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6027 return TraversalReturn::new_continue(());
6028 };
6029 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6030 }
6031 if let AstMutateCommand::DeleteNode = &ctx.command {
6032 return TraversalReturn {
6035 mutate_body_item: MutateBodyItem::Delete,
6036 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6037 };
6038 }
6039 }
6040 }
6041
6042 if ctx.command.needs_defined_names_stack() {
6043 if let NodeMut::Program(program) = &node {
6044 ctx.defined_names_stack.push(find_defined_names(*program));
6045 } else if let NodeMut::SketchBlock(block) = &node {
6046 ctx.defined_names_stack.push(find_defined_names(&block.body));
6047 }
6048 }
6049
6050 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6052 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6053 return TraversalReturn::new_continue(());
6054 }
6055 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6056 return TraversalReturn::new_continue(());
6057 };
6058 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6059}
6060
6061fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6062 match &ctx.node_path {
6063 Some(target) => Some(target) == node_path,
6064 None => node_range == ctx.source_range,
6065 }
6066}
6067
6068fn is_angle_constraint_call_name(name: &str) -> bool {
6069 matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6070}
6071
6072fn is_distance_constraint_call_name(name: &str) -> bool {
6073 matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6074}
6075
6076fn is_constraint_call_name(name: &str) -> bool {
6077 matches!(
6078 name,
6079 DISTANCE_FN
6080 | HORIZONTAL_DISTANCE_FN
6081 | VERTICAL_DISTANCE_FN
6082 | RADIUS_FN
6083 | DIAMETER_FN
6084 | ANGLE_FN
6085 | ANGLE_DIMENSION_FN
6086 )
6087}
6088
6089fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6090 if let ast::BinaryPart::CallExpressionKw(call) = part
6091 && is_constraint_call_name(call.callee.name.name.as_str())
6092 {
6093 Some(call)
6094 } else {
6095 None
6096 }
6097}
6098
6099fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6100 match &ctx.command {
6101 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6102 if let NodeMut::SketchBlock(sketch_block) = node {
6103 sketch_block
6104 .body
6105 .items
6106 .push(ast::BodyItem::ExpressionStatement(ast::Node {
6107 inner: ast::ExpressionStatement {
6108 expression: expr.clone(),
6109 digest: None,
6110 },
6111 start: Default::default(),
6112 end: Default::default(),
6113 module_id: Default::default(),
6114 node_path: None,
6115 outer_attrs: Default::default(),
6116 pre_comments: Default::default(),
6117 comment_start: Default::default(),
6118 }));
6119 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6120 }
6121 }
6122 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6123 if let NodeMut::SketchBlock(sketch_block) = node {
6124 let empty_defined_names = HashSet::new();
6125 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6126 let Ok(name) = next_free_name(prefix, defined_names) else {
6127 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6128 };
6129 sketch_block
6130 .body
6131 .items
6132 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6133 ast::VariableDeclaration::new(
6134 ast::VariableDeclarator::new(&name, expr.clone()),
6135 ast::ItemVisibility::Default,
6136 ast::VariableKind::Const,
6137 ),
6138 ))));
6139 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6140 }
6141 }
6142 AstMutateCommand::AddVariableDeclaration { prefix } => {
6143 if let NodeMut::VariableDeclaration(inner) = node {
6144 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6145 }
6146 if let NodeMut::ExpressionStatement(expr_stmt) = node {
6147 let empty_defined_names = HashSet::new();
6148 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6149 let Ok(name) = next_free_name(prefix, defined_names) else {
6150 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6152 };
6153 let mutate_node =
6154 ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6155 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6156 ast::ItemVisibility::Default,
6157 ast::VariableKind::Const,
6158 ))));
6159 return TraversalReturn {
6160 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6161 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6162 };
6163 }
6164 }
6165 AstMutateCommand::EditPoint { at } => {
6166 if let NodeMut::CallExpressionKw(call) = node {
6167 if call.callee.name.name != POINT_FN {
6168 return TraversalReturn::new_continue(());
6169 }
6170 for labeled_arg in &mut call.arguments {
6172 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6173 labeled_arg.arg = at.clone();
6174 }
6175 }
6176 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6177 }
6178 }
6179 AstMutateCommand::EditLine {
6180 start,
6181 end,
6182 construction,
6183 } => {
6184 if let NodeMut::CallExpressionKw(call) = node {
6185 if call.callee.name.name != LINE_FN {
6186 return TraversalReturn::new_continue(());
6187 }
6188 for labeled_arg in &mut call.arguments {
6190 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6191 labeled_arg.arg = start.clone();
6192 }
6193 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6194 labeled_arg.arg = end.clone();
6195 }
6196 }
6197 if let Some(construction_value) = construction {
6199 let construction_exists = call
6200 .arguments
6201 .iter()
6202 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6203 if *construction_value {
6204 if construction_exists {
6206 for labeled_arg in &mut call.arguments {
6208 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6209 labeled_arg.arg =
6210 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6211 value: ast::LiteralValue::Bool(true),
6212 raw: "true".to_string(),
6213 digest: None,
6214 })));
6215 }
6216 }
6217 } else {
6218 call.arguments.push(ast::LabeledArg {
6220 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6221 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6222 value: ast::LiteralValue::Bool(true),
6223 raw: "true".to_string(),
6224 digest: None,
6225 }))),
6226 });
6227 }
6228 } else {
6229 call.arguments
6231 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6232 }
6233 }
6234 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6235 }
6236 }
6237 AstMutateCommand::EditArc {
6238 start,
6239 end,
6240 center,
6241 direction,
6242 construction,
6243 } => {
6244 if let NodeMut::CallExpressionKw(call) = node {
6245 if call.callee.name.name != ARC_FN {
6246 return TraversalReturn::new_continue(());
6247 }
6248 for labeled_arg in &mut call.arguments {
6250 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6251 labeled_arg.arg = start.clone();
6252 }
6253 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6254 labeled_arg.arg = end.clone();
6255 }
6256 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6257 labeled_arg.arg = center.clone();
6258 }
6259 }
6260 if let Some(direction_value) = direction {
6262 let direction_exists = call
6263 .arguments
6264 .iter()
6265 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6266 if direction_value.is_clockwise() {
6267 let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6268 if direction_exists {
6269 for labeled_arg in &mut call.arguments {
6271 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6272 labeled_arg.arg = direction_ast.clone();
6273 }
6274 }
6275 } else {
6276 call.arguments.push(ast::LabeledArg {
6278 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6279 arg: direction_ast,
6280 });
6281 }
6282 } else {
6283 call.arguments
6286 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6287 }
6288 }
6289 if let Some(construction_value) = construction {
6291 let construction_exists = call
6292 .arguments
6293 .iter()
6294 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6295 if *construction_value {
6296 if construction_exists {
6298 for labeled_arg in &mut call.arguments {
6300 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6301 labeled_arg.arg =
6302 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6303 value: ast::LiteralValue::Bool(true),
6304 raw: "true".to_string(),
6305 digest: None,
6306 })));
6307 }
6308 }
6309 } else {
6310 call.arguments.push(ast::LabeledArg {
6312 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6313 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6314 value: ast::LiteralValue::Bool(true),
6315 raw: "true".to_string(),
6316 digest: None,
6317 }))),
6318 });
6319 }
6320 } else {
6321 call.arguments
6323 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6324 }
6325 }
6326 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6327 }
6328 }
6329 AstMutateCommand::EditCircle {
6330 start,
6331 center,
6332 construction,
6333 } => {
6334 if let NodeMut::CallExpressionKw(call) = node {
6335 if call.callee.name.name != CIRCLE_FN {
6336 return TraversalReturn::new_continue(());
6337 }
6338 for labeled_arg in &mut call.arguments {
6340 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6341 labeled_arg.arg = start.clone();
6342 }
6343 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6344 labeled_arg.arg = center.clone();
6345 }
6346 }
6347 if let Some(construction_value) = construction {
6349 let construction_exists = call
6350 .arguments
6351 .iter()
6352 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6353 if *construction_value {
6354 if construction_exists {
6355 for labeled_arg in &mut call.arguments {
6357 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6358 labeled_arg.arg =
6359 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6360 value: ast::LiteralValue::Bool(true),
6361 raw: "true".to_string(),
6362 digest: None,
6363 })));
6364 }
6365 }
6366 } else {
6367 call.arguments.push(ast::LabeledArg {
6369 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6370 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6371 value: ast::LiteralValue::Bool(true),
6372 raw: "true".to_string(),
6373 digest: None,
6374 }))),
6375 });
6376 }
6377 } else {
6378 call.arguments
6380 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6381 }
6382 }
6383 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6384 }
6385 }
6386 AstMutateCommand::EditControlPointSpline { points, construction } => {
6387 if let NodeMut::CallExpressionKw(call) = node {
6388 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6389 return TraversalReturn::new_continue(());
6390 }
6391 for labeled_arg in &mut call.arguments {
6392 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6393 {
6394 labeled_arg.arg = points.clone();
6395 }
6396 }
6397 if let Some(construction_value) = construction {
6399 let construction_exists = call
6400 .arguments
6401 .iter()
6402 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6403 if *construction_value {
6404 if construction_exists {
6405 for labeled_arg in &mut call.arguments {
6406 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6407 labeled_arg.arg =
6408 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6409 value: ast::LiteralValue::Bool(true),
6410 raw: "true".to_string(),
6411 digest: None,
6412 })));
6413 }
6414 }
6415 } else {
6416 call.arguments.push(ast::LabeledArg {
6417 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6418 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6419 value: ast::LiteralValue::Bool(true),
6420 raw: "true".to_string(),
6421 digest: None,
6422 }))),
6423 });
6424 }
6425 } else {
6426 call.arguments
6427 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6428 }
6429 }
6430 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6431 }
6432 }
6433 AstMutateCommand::EditConstraintValue { value } => {
6434 if let NodeMut::BinaryExpression(binary_expr) = node {
6435 let left_is_constraint = matches!(
6436 &binary_expr.left,
6437 ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6438 );
6439 if left_is_constraint {
6440 binary_expr.right = value.clone();
6441 } else {
6442 binary_expr.left = value.clone();
6443 }
6444
6445 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6446 }
6447 }
6448 AstMutateCommand::EditAngleConstraint { call, value } => {
6449 if let NodeMut::BinaryExpression(binary_expr) = node {
6450 let left_is_angle = matches!(
6451 &binary_expr.left,
6452 ast::BinaryPart::CallExpressionKw(existing_call)
6453 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6454 );
6455 let right_is_angle = matches!(
6456 &binary_expr.right,
6457 ast::BinaryPart::CallExpressionKw(existing_call)
6458 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6459 );
6460
6461 match (left_is_angle, right_is_angle) {
6462 (true, _) => {
6463 binary_expr.left = call.clone();
6464 binary_expr.right = value.clone();
6465 }
6466 (false, true) => {
6467 binary_expr.left = value.clone();
6468 binary_expr.right = call.clone();
6469 }
6470 (false, false) => return TraversalReturn::new_continue(()),
6471 }
6472
6473 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6474 }
6475 }
6476 AstMutateCommand::EditDistanceConstraint { call, value } => {
6477 if let NodeMut::BinaryExpression(binary_expr) = node {
6478 let left_is_distance = matches!(
6479 &binary_expr.left,
6480 ast::BinaryPart::CallExpressionKw(existing_call)
6481 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6482 );
6483 let right_is_distance = matches!(
6484 &binary_expr.right,
6485 ast::BinaryPart::CallExpressionKw(existing_call)
6486 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6487 );
6488
6489 match (left_is_distance, right_is_distance) {
6490 (true, _) => {
6491 binary_expr.left = call.clone();
6492 binary_expr.right = value.clone();
6493 }
6494 (false, true) => {
6495 binary_expr.left = value.clone();
6496 binary_expr.right = call.clone();
6497 }
6498 (false, false) => return TraversalReturn::new_continue(()),
6499 }
6500
6501 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6502 }
6503 }
6504 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6505 if let NodeMut::BinaryExpression(binary_expr) = node {
6506 let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6507 call
6508 } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6509 call
6510 } else {
6511 return TraversalReturn::new_continue(());
6512 };
6513
6514 if let Some(label_arg) = call
6515 .arguments
6516 .iter_mut()
6517 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6518 {
6519 label_arg.arg = label_position.clone();
6520 } else {
6521 call.arguments.push(ast::LabeledArg {
6522 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6523 arg: label_position.clone(),
6524 });
6525 }
6526
6527 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6528 }
6529 }
6530 AstMutateCommand::EditCallUnlabeled { arg } => {
6531 if let NodeMut::CallExpressionKw(call) = node {
6532 call.unlabeled = Some(arg.clone());
6533 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6534 }
6535 }
6536 AstMutateCommand::EditVarInitialValue { value } => {
6537 if let NodeMut::SketchVar(sketch_var) = node {
6541 let Ok(literal) = to_source_number(*value) else {
6542 return TraversalReturn::new_break(Err(KclError::refactor(format!(
6543 "Could not convert number to AST literal: {:?}",
6544 *value
6545 ))));
6546 };
6547 sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6548 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6549 }
6550 }
6551 AstMutateCommand::DeleteNode => {
6552 return TraversalReturn {
6553 mutate_body_item: MutateBodyItem::Delete,
6554 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6555 };
6556 }
6557 }
6558 TraversalReturn::new_continue(())
6559}
6560
6561struct FindSketchBlockSourceRange {
6562 target_before_mutation: SourceRange,
6564 found: Cell<Option<AstNodeRef>>,
6568}
6569
6570impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6571 type Error = crate::front::Error;
6572
6573 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6574 let Ok(node_range) = SourceRange::try_from(&node) else {
6575 return Ok(true);
6576 };
6577
6578 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6579 if node_range.module_id() == self.target_before_mutation.module_id()
6580 && node_range.start() == self.target_before_mutation.start()
6581 && node_range.end() >= self.target_before_mutation.end()
6583 {
6584 self.found.set(sketch_block.body.items.last().map(|item| match item {
6585 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6589 _ => AstNodeRef::from(item),
6590 }));
6591 return Ok(false);
6592 } else {
6593 return Ok(true);
6596 }
6597 }
6598
6599 for child in node.children().iter() {
6600 if !child.visit(*self)? {
6601 return Ok(false);
6602 }
6603 }
6604
6605 Ok(true)
6606 }
6607}
6608
6609struct FindSketchBlockByNodePath {
6610 target_node_path: ast::NodePath,
6612 found: Cell<Option<AstNodeRef>>,
6616}
6617
6618impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6619 type Error = crate::front::Error;
6620
6621 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6622 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6623 return Ok(true);
6624 };
6625
6626 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6627 if let Some(node_path) = node_path
6628 && node_path == self.target_node_path
6629 {
6630 self.found.set(sketch_block.body.items.last().map(|item| match item {
6631 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6635 _ => AstNodeRef::from(item),
6636 }));
6637
6638 return Ok(false);
6639 } else {
6640 return Ok(true);
6643 }
6644 }
6645
6646 for child in node.children().iter() {
6647 if !child.visit(*self)? {
6648 return Ok(false);
6649 }
6650 }
6651
6652 Ok(true)
6653 }
6654}
6655
6656fn find_sketch_block_added_item(
6664 ast: &ast::Node<ast::Program>,
6665 sketch_block_before_mutation: &AstNodeRef,
6666) -> Result<AstNodeRef, KclError> {
6667 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6668 let find = FindSketchBlockByNodePath {
6669 target_node_path: node_path.clone(),
6670 found: Cell::new(None),
6671 };
6672 let node = crate::walk::Node::from(ast);
6673 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6674 find.found.into_inner().ok_or_else(|| {
6675 KclError::refactor(format!(
6676 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6677 ))
6678 })
6679 } else {
6680 let find = FindSketchBlockSourceRange {
6682 target_before_mutation: sketch_block_before_mutation.range,
6683 found: Cell::new(None),
6684 };
6685 let node = crate::walk::Node::from(ast);
6686 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6687 find.found.into_inner().ok_or_else(|| KclError::refactor(
6688 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?"),
6689 ))
6690 }
6691}
6692
6693fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6694 let message = error.message().trim();
6695 let message = if message.is_empty() {
6696 "unknown parse error"
6697 } else {
6698 message
6699 };
6700
6701 format!("{prefix}: {message}")
6702}
6703
6704fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6705 let (program, errors) = Program::parse(source).map_err(|err| {
6706 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6707 })?;
6708 if !errors.is_empty() {
6709 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6710 format_compilation_issues(parse_error_prefix, &errors),
6711 )));
6712 }
6713
6714 program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6715}
6716
6717fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6718 let Some(first_issue) = issues
6719 .iter()
6720 .find(|issue| issue.severity.is_err())
6721 .or_else(|| issues.first())
6722 else {
6723 return prefix.to_owned();
6724 };
6725
6726 let message = first_issue.message.trim();
6727 let message = if message.is_empty() {
6728 "unknown parse error"
6729 } else {
6730 message
6731 };
6732
6733 if issues.len() > 1 {
6734 format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6735 } else {
6736 format!("{prefix}: {message}")
6737 }
6738}
6739
6740fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6741 ast.recast_top(&Default::default(), 0)
6743}
6744
6745struct FindNumericLiteral {
6746 target: SourceRange,
6747 found: Cell<Option<ast::NumericLiteral>>,
6748}
6749
6750impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6751 type Error = crate::front::Error;
6752
6753 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6754 let Ok(node_range) = SourceRange::try_from(&node) else {
6755 return Ok(true);
6756 };
6757
6758 if node_range == self.target
6759 && let crate::walk::Node::NumericLiteral(literal) = node
6760 {
6761 self.found.set(Some(literal.inner.clone()));
6762 return Ok(false);
6763 }
6764
6765 for child in node.children().iter() {
6766 if !child.visit(*self)? {
6767 return Ok(false);
6768 }
6769 }
6770
6771 Ok(true)
6772 }
6773}
6774
6775fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6776 let find = FindNumericLiteral {
6777 target,
6778 found: Cell::new(None),
6779 };
6780 let node = crate::walk::Node::from(ast);
6781 node.visit(&find).ok()?;
6782 find.found.into_inner()
6783}
6784
6785struct FindSketchVarInitialByNodePath<'a> {
6786 target: &'a ast::NodePath,
6787 sketch_var_found: Cell<bool>,
6788 initial_literal: Cell<Option<ast::NumericLiteral>>,
6789}
6790
6791impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6792 type Error = crate::front::Error;
6793
6794 fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6795 if let crate::walk::Node::SketchVar(sketch_var) = node
6796 && sketch_var.node_path.as_ref() == Some(self.target)
6797 {
6798 self.sketch_var_found.set(true);
6799 if let Some(initial) = &sketch_var.initial {
6800 self.initial_literal.set(Some(initial.inner.clone()));
6801 }
6802 return Ok(false);
6803 }
6804
6805 for child in node.children().iter() {
6806 if !child.visit(*self)? {
6807 return Ok(false);
6808 }
6809 }
6810
6811 Ok(true)
6812 }
6813}
6814
6815fn numeric_literal_at_node_path(
6825 ast: &ast::Node<ast::Program>,
6826 node_path: Option<&ast::NodePath>,
6827 source_range: SourceRange,
6828) -> Option<Option<ast::NumericLiteral>> {
6829 let Some(node_path) = node_path else {
6830 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";
6831 #[cfg(target_arch = "wasm32")]
6832 web_sys::console::warn_1(&message.into());
6833 #[cfg(not(target_arch = "wasm32"))]
6834 eprintln!("WARNING: {message}");
6835 return numeric_literal_at_source_range(ast, source_range).map(Some);
6836 };
6837 let find = FindSketchVarInitialByNodePath {
6838 target: node_path,
6839 sketch_var_found: Cell::new(false),
6840 initial_literal: Cell::new(None),
6841 };
6842 let node = crate::walk::Node::from(ast);
6843 node.visit(&find).ok()?;
6844 if !find.sketch_var_found.get() {
6845 return None;
6846 }
6847 Some(find.initial_literal.into_inner())
6848}
6849
6850fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6851 match suffix {
6852 NumericSuffix::Mm => Some(UnitLength::Millimeters),
6853 NumericSuffix::Cm => Some(UnitLength::Centimeters),
6854 NumericSuffix::M => Some(UnitLength::Meters),
6855 NumericSuffix::Inch => Some(UnitLength::Inches),
6856 NumericSuffix::Ft => Some(UnitLength::Feet),
6857 NumericSuffix::Yd => Some(UnitLength::Yards),
6858 _ => None,
6859 }
6860}
6861
6862fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6863 match suffix_length_unit(number.units) {
6864 Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6865 None => number.value,
6866 }
6867}
6868
6869fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6870 match suffix_length_unit(literal.suffix) {
6871 Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6872 None => literal.value,
6873 }
6874}
6875
6876fn var_solution_needs_commit(
6877 current_literal: &ast::NumericLiteral,
6878 solved_value: Number,
6879 default_length_unit: UnitLength,
6880) -> bool {
6881 let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6882 let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6883
6884 (current - solved).abs() > 1e-9
6885}
6886
6887fn preserve_var_solution_literal_style(
6888 current_literal: &ast::NumericLiteral,
6889 solved_value: Number,
6890 default_length_unit: UnitLength,
6891) -> Number {
6892 if current_literal.suffix == NumericSuffix::None {
6893 return Number {
6894 value: number_value_in_default_length_units(solved_value, default_length_unit),
6895 units: NumericSuffix::None,
6896 };
6897 }
6898
6899 let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6900 return solved_value;
6901 };
6902
6903 let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6904 Number {
6905 value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6906 units: current_literal.suffix,
6907 }
6908}
6909
6910pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6911 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6912 inner: ast::ArrayExpression {
6913 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6914 non_code_meta: Default::default(),
6915 digest: None,
6916 },
6917 start: Default::default(),
6918 end: Default::default(),
6919 module_id: Default::default(),
6920 node_path: None,
6921 outer_attrs: Default::default(),
6922 pre_comments: Default::default(),
6923 comment_start: Default::default(),
6924 })))
6925}
6926
6927pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6928 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6929 ast::ArrayExpression {
6930 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6931 digest: None,
6932 non_code_meta: Default::default(),
6933 },
6934 ))))
6935}
6936
6937fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6938 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6939 ast::ArrayExpression {
6940 elements: vec![
6941 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6942 point.x,
6943 )?)))),
6944 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6945 point.y,
6946 )?)))),
6947 ],
6948 non_code_meta: Default::default(),
6949 digest: None,
6950 },
6951 ))))
6952}
6953
6954fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6955 match expr {
6956 Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6957 inner: ast::Literal::from(to_source_number(*number)?),
6958 start: Default::default(),
6959 end: Default::default(),
6960 module_id: Default::default(),
6961 node_path: None,
6962 outer_attrs: Default::default(),
6963 pre_comments: Default::default(),
6964 comment_start: Default::default(),
6965 }))),
6966 Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6967 inner: ast::SketchVar {
6968 initial: Some(BoxNode::new(ast::Node {
6969 inner: to_source_number(*number)?,
6970 start: Default::default(),
6971 end: Default::default(),
6972 module_id: Default::default(),
6973 node_path: None,
6974 outer_attrs: Default::default(),
6975 pre_comments: Default::default(),
6976 comment_start: Default::default(),
6977 })),
6978 digest: None,
6979 },
6980 start: Default::default(),
6981 end: Default::default(),
6982 module_id: Default::default(),
6983 node_path: None,
6984 outer_attrs: Default::default(),
6985 pre_comments: Default::default(),
6986 comment_start: Default::default(),
6987 }))),
6988 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6989 }
6990}
6991
6992fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6993 Ok(ast::NumericLiteral {
6994 value: number.value,
6995 suffix: number.units,
6996 raw: format_number_literal(number.value, number.units, None)?,
6997 digest: None,
6998 })
6999}
7000
7001pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7002 ast::Expr::Name(BoxNode::new(ast_name(name)))
7003}
7004
7005fn ast_name(name: String) -> ast::Node<ast::Name> {
7006 ast::Node {
7007 inner: ast::Name {
7008 name: ast::Node {
7009 inner: ast::Identifier { name, digest: None },
7010 start: Default::default(),
7011 end: Default::default(),
7012 module_id: Default::default(),
7013 node_path: None,
7014 outer_attrs: Default::default(),
7015 pre_comments: Default::default(),
7016 comment_start: Default::default(),
7017 },
7018 path: Vec::new(),
7019 abs_path: false,
7020 digest: None,
7021 },
7022 start: Default::default(),
7023 end: Default::default(),
7024 module_id: Default::default(),
7025 node_path: None,
7026 outer_attrs: Default::default(),
7027 pre_comments: Default::default(),
7028 comment_start: Default::default(),
7029 }
7030}
7031
7032pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7033 ast::Name {
7034 name: ast::Node {
7035 inner: ast::Identifier {
7036 name: name.to_owned(),
7037 digest: None,
7038 },
7039 start: Default::default(),
7040 end: Default::default(),
7041 module_id: Default::default(),
7042 node_path: None,
7043 outer_attrs: Default::default(),
7044 pre_comments: Default::default(),
7045 comment_start: Default::default(),
7046 },
7047 path: Default::default(),
7048 abs_path: false,
7049 digest: None,
7050 }
7051}
7052
7053pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
7055 let elements = exprs.into_iter().collect::<Vec<_>>();
7056 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
7057
7058 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7060 elements,
7061 digest: None,
7062 non_code_meta: Default::default(),
7063 })));
7064
7065 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7067 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
7068 unlabeled: Some(array_expr),
7069 arguments: Default::default(),
7070 digest: None,
7071 non_code_meta: Default::default(),
7072 })))
7073}
7074
7075pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7077 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7078 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7079 unlabeled: Some(line_expr),
7080 arguments: Default::default(),
7081 digest: None,
7082 non_code_meta: Default::default(),
7083 })))
7084}
7085
7086pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7088 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7089 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7090 unlabeled: Some(line_expr),
7091 arguments: Default::default(),
7092 digest: None,
7093 non_code_meta: Default::default(),
7094 })))
7095}
7096
7097pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7099 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7100 object: object_expr,
7101 property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7102 name: ast::Node::no_src(ast::Identifier {
7103 name: property.to_string(),
7104 digest: None,
7105 }),
7106 path: Vec::new(),
7107 abs_path: false,
7108 digest: None,
7109 }))),
7110 computed: false,
7111 digest: None,
7112 })))
7113}
7114
7115pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7116 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7117 object: object_expr,
7118 property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7119 ast::NumericLiteral {
7120 value: index as f64,
7121 suffix: NumericSuffix::None,
7122 raw: index.to_string(),
7123 digest: None,
7124 },
7125 )))),
7126 computed: true,
7127 digest: None,
7128 })))
7129}
7130
7131fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7133 let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7135 position.x,
7136 )?))));
7137 let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7138 position.y,
7139 )?))));
7140 let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7141 elements: vec![x_literal, y_literal],
7142 digest: None,
7143 non_code_meta: Default::default(),
7144 })));
7145
7146 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7148 elements: vec![point_expr, point_array],
7149 digest: None,
7150 non_code_meta: Default::default(),
7151 })));
7152
7153 Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7155 ast::CallExpressionKw {
7156 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7157 unlabeled: Some(array_expr),
7158 arguments: Default::default(),
7159 digest: None,
7160 non_code_meta: Default::default(),
7161 },
7162 ))))
7163}
7164
7165pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7167 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7168 elements: line_exprs,
7169 digest: None,
7170 non_code_meta: Default::default(),
7171 })));
7172
7173 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7175 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7176 unlabeled: Some(array_expr),
7177 arguments: Default::default(),
7178 digest: None,
7179 non_code_meta: Default::default(),
7180 })))
7181}
7182
7183pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7185 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7186 elements: segment_exprs,
7187 digest: None,
7188 non_code_meta: Default::default(),
7189 })));
7190
7191 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7192 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7193 unlabeled: Some(array_expr),
7194 arguments: Default::default(),
7195 digest: None,
7196 non_code_meta: Default::default(),
7197 })))
7198}
7199
7200pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7202 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7203 elements: vec![seg1_expr, seg2_expr],
7204 digest: None,
7205 non_code_meta: Default::default(),
7206 })));
7207
7208 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7209 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7210 unlabeled: Some(array_expr),
7211 arguments: Default::default(),
7212 digest: None,
7213 non_code_meta: Default::default(),
7214 })))
7215}
7216
7217pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7219 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7220 elements: input_exprs,
7221 digest: None,
7222 non_code_meta: Default::default(),
7223 })));
7224 let arguments = vec![ast::LabeledArg {
7225 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7226 arg: axis_expr,
7227 }];
7228
7229 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7230 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7231 unlabeled: Some(array_expr),
7232 arguments,
7233 digest: None,
7234 non_code_meta: Default::default(),
7235 })))
7236}
7237
7238pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7240 let arguments = vec![ast::LabeledArg {
7241 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7242 arg: point_expr,
7243 }];
7244
7245 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7246 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7247 unlabeled: Some(segment_expr),
7248 arguments,
7249 digest: None,
7250 non_code_meta: Default::default(),
7251 })))
7252}
7253
7254fn issue_source_range(error: &KclError) -> SourceRange {
7259 let source_ranges = error.source_ranges();
7260 source_ranges
7261 .iter()
7262 .find(|range| range.is_top_level_module())
7263 .or_else(|| source_ranges.first())
7264 .copied()
7265 .unwrap_or_else(SourceRange::synthetic)
7266}
7267
7268#[cfg(test)]
7269mod tests {
7270 use std::sync;
7271
7272 use super::*;
7273 use crate::engine::PlaneName;
7274 use crate::engine::engine_manager::EngineManager;
7275 use crate::execution::cache::SketchModeState;
7276 use crate::execution::cache::clear_mem_cache;
7277 use crate::execution::cache::read_old_memory;
7278 use crate::execution::cache::write_old_memory;
7279 use crate::front::Distance;
7280 use crate::front::Fixed;
7281 use crate::front::FixedPoint;
7282 use crate::front::Midpoint;
7283 use crate::front::Object;
7284 use crate::front::Plane;
7285 use crate::front::Sketch;
7286 use crate::front::Tangent;
7287 use crate::frontend::sketch::Vertical;
7288 use crate::pretty::NumericSuffix;
7289
7290 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7291 for object in &scene_graph.objects {
7292 if let ObjectKind::Sketch(_) = &object.kind {
7293 return Some(object);
7294 }
7295 }
7296 None
7297 }
7298
7299 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7300 for object in &scene_graph.objects {
7301 if let ObjectKind::Face(_) = &object.kind {
7302 return Some(object);
7303 }
7304 }
7305 None
7306 }
7307
7308 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7309 for object in &scene_graph.objects {
7310 if matches!(&object.kind, ObjectKind::Wall(_)) {
7311 return Some(object.id);
7312 }
7313 }
7314 None
7315 }
7316
7317 fn find_cap_object_id_with_solid_output_index(
7318 scene_graph: &SceneGraph,
7319 cap_kind: crate::frontend::api::CapKind,
7320 solid_output_index: usize,
7321 ) -> Option<ObjectId> {
7322 for object in &scene_graph.objects {
7323 if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7324 {
7325 return Some(object.id);
7326 }
7327 }
7328 None
7329 }
7330
7331 #[test]
7332 fn issue_source_range_prefers_top_level_module() {
7333 use kcl_error::ModuleId;
7334
7335 let top = SourceRange::new(10, 20, ModuleId::default());
7336 let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7337
7338 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7341 "boom".to_owned(),
7342 vec![imported, top],
7343 ));
7344 assert_eq!(super::issue_source_range(&error), top);
7345
7346 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7348 assert_eq!(super::issue_source_range(&error), imported);
7349
7350 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7352 assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7353 }
7354
7355 #[test]
7356 fn composite_constituent_sweeps_are_not_solid_outputs() {
7357 use kcl_api::artifact::ArtifactSweepMethod;
7358 use kcl_api::artifact::CompositeSolid;
7359 use kcl_api::artifact::CompositeSolidSubType;
7360 use kcl_api::artifact::Sweep;
7361 use kcl_api::artifact::SweepSubType;
7362
7363 let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7364 let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7365 let composite_id = ArtifactId::new(Uuid::new_v4());
7366 let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7367 let sweep = |id| {
7368 Artifact::Sweep(Sweep {
7369 id,
7370 sub_type: SweepSubType::Extrusion,
7371 path_id: ArtifactId::new(Uuid::new_v4()),
7372 surface_ids: Vec::new(),
7373 edge_ids: Vec::new(),
7374 code_ref: code_ref.clone(),
7375 source_sweep_id: None,
7376 trajectory_id: None,
7377 method: ArtifactSweepMethod::New,
7378 consumed: false,
7379 pattern_ids: Vec::new(),
7380 })
7381 };
7382 let mut artifacts = IndexMap::from([
7383 (first_sweep_id, sweep(first_sweep_id)),
7384 (second_sweep_id, sweep(second_sweep_id)),
7385 ]);
7386
7387 let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7388 assert_eq!(
7389 solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7390 Some(0)
7391 );
7392 assert_eq!(
7393 solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7394 Some(1)
7395 );
7396
7397 artifacts.insert(
7398 composite_id,
7399 Artifact::CompositeSolid(CompositeSolid {
7400 id: composite_id,
7401 consumed: false,
7402 sub_type: CompositeSolidSubType::Union,
7403 output_index: None,
7404 solid_ids: vec![first_sweep_id, second_sweep_id],
7405 tool_ids: Vec::new(),
7406 code_ref,
7407 composite_solid_id: None,
7408 pattern_ids: Vec::new(),
7409 }),
7410 );
7411 let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7412 assert_eq!(
7413 solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7414 None
7415 );
7416 assert_eq!(
7417 solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7418 None
7419 );
7420 }
7421
7422 #[test]
7423 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7424 let source = "\
7425region001 = region(point = [0.1, 0.1], sketch = s)
7426extrude001 = extrude(region001, length = 5)
7427revolve001 = revolve(region001, axis = Y)
7428sweep001 = sweep(region001, path = path001)
7429loft001 = loft(region001)
7430not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7431";
7432
7433 let program = Program::parse(source).unwrap().0.unwrap();
7434
7435 assert_eq!(
7436 region_name_from_sweep_variable(&program.ast, "extrude001"),
7437 Some("region001".to_owned())
7438 );
7439 assert_eq!(
7440 region_name_from_sweep_variable(&program.ast, "revolve001"),
7441 Some("region001".to_owned())
7442 );
7443 assert_eq!(
7444 region_name_from_sweep_variable(&program.ast, "sweep001"),
7445 Some("region001".to_owned())
7446 );
7447 assert_eq!(
7448 region_name_from_sweep_variable(&program.ast, "loft001"),
7449 Some("region001".to_owned())
7450 );
7451 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7452 }
7453
7454 #[track_caller]
7455 fn expect_sketch(object: &Object) -> &Sketch {
7456 if let ObjectKind::Sketch(sketch) = &object.kind {
7457 sketch
7458 } else {
7459 panic!("Object is not a sketch: {:?}", object);
7460 }
7461 }
7462
7463 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7464 let point_object = scene_graph.objects.get(point_id.0).unwrap();
7465 let ObjectKind::Segment {
7466 segment: Segment::Point(point),
7467 } = &point_object.kind
7468 else {
7469 panic!("Object is not a point segment: {point_object:?}");
7470 };
7471 point.position.clone()
7472 }
7473
7474 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7475 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7476 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7477 }
7478
7479 fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7482 Point2d {
7483 x: Expr::Var(Number {
7484 value: x,
7485 units: NumericSuffix::Mm,
7486 }),
7487 y: Expr::Var(Number {
7488 value: y,
7489 units: NumericSuffix::Mm,
7490 }),
7491 }
7492 }
7493
7494 fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7497 Point2d {
7498 x: Number {
7499 value: x,
7500 units: NumericSuffix::Mm,
7501 },
7502 y: Number {
7503 value: y,
7504 units: NumericSuffix::Mm,
7505 },
7506 }
7507 }
7508
7509 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7510 LineCtor {
7511 start: Point2d {
7512 x: Expr::Number(Number { value: start_x, units }),
7513 y: Expr::Number(Number { value: start_y, units }),
7514 },
7515 end: Point2d {
7516 x: Expr::Number(Number { value: end_x, units }),
7517 y: Expr::Number(Number { value: end_y, units }),
7518 },
7519 construction: None,
7520 }
7521 }
7522
7523 async fn create_sketch_with_single_line(
7524 frontend: &mut FrontendState,
7525 ctx: &ExecutorContext,
7526 mock_ctx: &ExecutorContext,
7527 version: Version,
7528 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7529 frontend.program = Program::empty();
7530
7531 let sketch_args = SketchCtor {
7532 on: Plane::Default(PlaneName::Xy),
7533 };
7534 let (_src_delta, _scene_delta, sketch_id) = frontend
7535 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7536 .await
7537 .unwrap();
7538
7539 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7540 let (source_delta, scene_graph_delta) = frontend
7541 .add_segment(mock_ctx, version, sketch_id, segment, None)
7542 .await
7543 .unwrap();
7544 let line_id = *scene_graph_delta
7545 .new_objects
7546 .last()
7547 .expect("Expected line object id to be created");
7548
7549 (sketch_id, line_id, source_delta, scene_graph_delta)
7550 }
7551
7552 async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7553 frontend.program = program.clone();
7554 let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7555 frontend.update_state_after_exec(outcome, true);
7556 }
7557
7558 #[test]
7559 fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7560 for (source, expected_message) in [
7561 ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7562 ("3'", "Error parsing KCL source after editing: found unknown token '''"),
7563 ] {
7564 let err = parse_frontend_mutation_source(
7565 source,
7566 "Error parsing KCL source after editing",
7567 "No AST produced after editing",
7568 )
7569 .expect_err("expected invalid KCL source to fail");
7570 let message = err.error.message();
7571
7572 assert_eq!(message, expected_message);
7573 assert!(!message.contains("CompilationIssue"));
7574 assert!(!message.contains("KclErrorDetails"));
7575 assert!(!message.contains("source_range"));
7576 }
7577 }
7578
7579 #[tokio::test(flavor = "multi_thread")]
7580 async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7581 let initial_source = "\
7582sketch(on = XY) {
7583 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7584 distance([line1.start, line1.end]) == 10
7585}
7586";
7587 let program = Program::parse(initial_source).unwrap().0.unwrap();
7588
7589 let mut frontend = FrontendState::new();
7590 let mock_ctx = ExecutorContext::new_mock(None).await;
7591 let version = Version(0);
7592
7593 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7594 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7595 let sketch_id = sketch_object.id;
7596 let sketch = expect_sketch(sketch_object);
7597 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7598
7599 for (value, expected_message) in [
7600 ("**", "Invalid constraint value: Unexpected token: *"),
7601 ("3'", "Invalid constraint value: found unknown token '''"),
7602 ] {
7603 let err = frontend
7604 .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7605 .await
7606 .expect_err("expected invalid constraint expression to fail");
7607 let message = err.error.message();
7608
7609 assert_eq!(message, expected_message);
7610 assert!(!message.contains("CompilationIssue"));
7611 assert!(!message.contains("KclErrorDetails"));
7612 assert!(!message.contains("source_range"));
7613 }
7614
7615 mock_ctx.close().await;
7616 }
7617
7618 #[tokio::test(flavor = "multi_thread")]
7619 async fn test_failed_edit_constraint_value_does_not_update_program() {
7620 let initial_source = "\
7621sketch(on = XY) {
7622 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7623 distance([line1.start, line1.end]) == 10
7624}
7625";
7626 let program = Program::parse(initial_source).unwrap().0.unwrap();
7627 let original_source = program.original_file_contents.clone();
7628
7629 let mut frontend = FrontendState::new();
7630 let mock_ctx = ExecutorContext::new_mock(None).await;
7631 let version = Version(0);
7632
7633 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7634 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7635 let sketch_id = sketch_object.id;
7636 let sketch = expect_sketch(sketch_object);
7637 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7638
7639 frontend
7640 .edit_constraint_value(
7641 &mock_ctx,
7642 version,
7643 sketch_id,
7644 constraint_id,
7645 "unknownDistance".to_owned(),
7646 )
7647 .await
7648 .expect_err("expected invalid constraint value to fail execution");
7649
7650 assert_eq!(frontend.program.original_file_contents, original_source);
7651 assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7652
7653 mock_ctx.close().await;
7654 }
7655
7656 #[tokio::test(flavor = "multi_thread")]
7657 async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7658 let initial_source = "\
7659arr = [0]
7660sketch(on = XY) {
7661 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7662 distance([line1.start, line1.end]) == 10
7663}
7664";
7665 let program = Program::parse(initial_source).unwrap().0.unwrap();
7666
7667 let mut frontend = FrontendState::new();
7668 let mock_ctx = ExecutorContext::new_mock(None).await;
7669 let version = Version(0);
7670
7671 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7672 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7673 let sketch_id = sketch_object.id;
7674 let sketch = expect_sketch(sketch_object);
7675 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7676
7677 let err = frontend
7681 .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7682 .await
7683 .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7684 let message = err.error.message();
7685 assert!(
7686 message.contains("The array doesn't have any item at index 5"),
7687 "unexpected error message: {message}"
7688 );
7689
7690 mock_ctx.close().await;
7691 }
7692
7693 #[tokio::test(flavor = "multi_thread")]
7694 async fn test_sketch_checkpoint_round_trip_restores_state() {
7695 let mut frontend = FrontendState::new();
7696 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7697 let mock_ctx = ExecutorContext::new_mock(None).await;
7698 let version = Version(0);
7699
7700 let (sketch_id, line_id, source_delta, scene_graph_delta) =
7701 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7702
7703 let expected_source = source_delta.text.clone();
7704 let expected_scene_graph = frontend.scene_graph.clone();
7705 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7706 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7707
7708 let checkpoint_id = frontend
7709 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7710 .await
7711 .unwrap();
7712
7713 let edited_segments = vec![ExistingSegmentCtor {
7714 id: line_id,
7715 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7716 }];
7717 let (edited_source, _edited_scene) = frontend
7718 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7719 .await
7720 .unwrap();
7721 assert_ne!(edited_source.text, expected_source);
7722
7723 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7724
7725 assert_eq!(restored.source_delta.text, expected_source);
7726 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7727 assert!(restored.scene_graph_delta.invalidates_ids);
7728 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7729 assert_eq!(frontend.scene_graph, expected_scene_graph);
7730 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7731
7732 ctx.close().await;
7733 }
7734
7735 #[tokio::test(flavor = "multi_thread")]
7736 async fn test_sketch_checkpoints_prune_oldest_entries() {
7737 let mut frontend = FrontendState::new();
7738 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7739 let mock_ctx = ExecutorContext::new_mock(None).await;
7740 let version = Version(0);
7741
7742 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7743 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7744
7745 let mut checkpoint_ids = Vec::new();
7746 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7747 checkpoint_ids.push(
7748 frontend
7749 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7750 .await
7751 .unwrap(),
7752 );
7753 }
7754
7755 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7756 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7757
7758 let oldest_retained = checkpoint_ids[3];
7759 assert_eq!(
7760 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7761 Some(oldest_retained)
7762 );
7763
7764 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7765 assert!(evicted_restore.is_err());
7766 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7767
7768 frontend
7769 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7770 .await
7771 .unwrap();
7772
7773 ctx.close().await;
7774 }
7775
7776 #[tokio::test(flavor = "multi_thread")]
7777 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7778 let mut frontend = FrontendState::new();
7779 let missing_checkpoint = SketchCheckpointId::new(999);
7780
7781 let err = frontend
7782 .restore_sketch_checkpoint(missing_checkpoint)
7783 .await
7784 .expect_err("Expected restore to fail for missing checkpoint");
7785
7786 assert!(err.msg.contains("Sketch checkpoint not found"));
7787 }
7788
7789 #[tokio::test(flavor = "multi_thread")]
7790 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7791 let mut frontend = FrontendState::new();
7792 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7793 let mock_ctx = ExecutorContext::new_mock(None).await;
7794 let version = Version(0);
7795
7796 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7797 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7798
7799 let checkpoint_a = frontend
7800 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7801 .await
7802 .unwrap();
7803 let checkpoint_b = frontend
7804 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7805 .await
7806 .unwrap();
7807 assert_eq!(frontend.sketch_checkpoints.len(), 2);
7808
7809 frontend.clear_sketch_checkpoints();
7810 assert!(frontend.sketch_checkpoints.is_empty());
7811 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7812 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7813
7814 ctx.close().await;
7815 }
7816
7817 #[tokio::test(flavor = "multi_thread")]
7818 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7819 let mut frontend = FrontendState::new();
7820 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7821 let mock_ctx = ExecutorContext::new_mock(None).await;
7822 let version = Version(0);
7823
7824 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7825 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7826 let old_source = source_delta.text.clone();
7827 let old_checkpoint = frontend
7828 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7829 .await
7830 .unwrap();
7831 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7832
7833 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
7834 .unwrap()
7835 .0
7836 .unwrap();
7837
7838 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7839 let SetProgramOutcome::Success {
7840 checkpoint_id: Some(new_checkpoint),
7841 ..
7842 } = result
7843 else {
7844 panic!("Expected Success with a fresh checkpoint baseline");
7845 };
7846
7847 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7848
7849 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7850 assert_eq!(old_restore.source_delta.text, old_source);
7851
7852 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7853 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7854
7855 ctx.close().await;
7856 }
7857
7858 #[tokio::test(flavor = "multi_thread")]
7859 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7860 let mut frontend = FrontendState::new();
7861 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7862 let mock_ctx = ExecutorContext::new_mock(None).await;
7863 let version = Version(0);
7864
7865 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7866 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7867 let old_checkpoint = frontend
7868 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7869 .await
7870 .unwrap();
7871 let checkpoint_count_before = frontend.sketch_checkpoints.len();
7872
7873 let failing_program = Program::parse(
7874 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7875 )
7876 .unwrap()
7877 .0
7878 .unwrap();
7879
7880 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7881 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7882 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7883 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7884
7885 ctx.close().await;
7886 }
7887
7888 #[tokio::test(flavor = "multi_thread")]
7889 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7890 let mut frontend = FrontendState::new();
7891 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7892
7893 let program = Program::parse(
7894 "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",
7895 )
7896 .unwrap()
7897 .0
7898 .unwrap();
7899 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7900 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7901 panic!("Expected successful baseline program execution");
7902 };
7903
7904 clear_mem_cache().await;
7905 assert!(read_old_memory().await.is_none());
7906
7907 let checkpoint_without_mock_memory = frontend
7908 .create_sketch_checkpoint((*exec_outcome).clone())
7909 .await
7910 .unwrap();
7911
7912 write_old_memory(SketchModeState::new_for_tests()).await;
7913 assert!(read_old_memory().await.is_some());
7914
7915 let checkpoint_with_mock_memory = frontend
7916 .create_sketch_checkpoint((*exec_outcome).clone())
7917 .await
7918 .unwrap();
7919
7920 clear_mem_cache().await;
7921 assert!(read_old_memory().await.is_none());
7922
7923 frontend
7924 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7925 .await
7926 .unwrap();
7927 assert!(read_old_memory().await.is_some());
7928
7929 frontend
7930 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7931 .await
7932 .unwrap();
7933 assert!(read_old_memory().await.is_none());
7934
7935 ctx.close().await;
7936 }
7937
7938 #[tokio::test(flavor = "multi_thread")]
7939 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7940 let source = "\
7941sketch(on = XY) {
7942 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7943}
7944
7945bad = missing_name
7946";
7947 let program = Program::parse(source).unwrap().0.unwrap();
7948
7949 let mut frontend = FrontendState::new();
7950
7951 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7952 let mock_ctx = ExecutorContext::new_mock(None).await;
7953 let version = Version(0);
7954 let project_id = ProjectId(0);
7955 let file_id = FileId(0);
7956
7957 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7958 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7959 };
7960
7961 let sketch_id = frontend
7962 .scene_graph
7963 .objects
7964 .iter()
7965 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7966 .expect("Expected sketch object from errored hack_set_program");
7967
7968 frontend
7969 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7970 .await
7971 .unwrap();
7972
7973 ctx.close().await;
7974 mock_ctx.close().await;
7975 }
7976
7977 #[tokio::test(flavor = "multi_thread")]
7978 async fn test_new_sketch_add_point_edit_point() {
7979 let program = Program::empty();
7980
7981 let mut frontend = FrontendState::new();
7982 frontend.program = program;
7983
7984 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7985 let mock_ctx = ExecutorContext::new_mock(None).await;
7986 let version = Version(0);
7987
7988 let sketch_args = SketchCtor {
7989 on: Plane::Default(PlaneName::Xy),
7990 };
7991 let (_src_delta, scene_delta, sketch_id) = frontend
7992 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7993 .await
7994 .unwrap();
7995 assert_eq!(sketch_id, ObjectId(1));
7996 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7997 let sketch_object = &scene_delta.new_graph.objects[1];
7998 assert_eq!(sketch_object.id, ObjectId(1));
7999 assert_eq!(
8000 sketch_object.kind,
8001 ObjectKind::Sketch(Sketch {
8002 args: SketchCtor {
8003 on: Plane::Default(PlaneName::Xy)
8004 },
8005 plane: ObjectId(0),
8006 segments: vec![],
8007 constraints: vec![],
8008 })
8009 );
8010 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8011
8012 let point_ctor = PointCtor {
8013 position: Point2d {
8014 x: Expr::Number(Number {
8015 value: 1.0,
8016 units: NumericSuffix::Inch,
8017 }),
8018 y: Expr::Number(Number {
8019 value: 2.0,
8020 units: NumericSuffix::Inch,
8021 }),
8022 },
8023 };
8024 let segment = SegmentCtor::Point(point_ctor);
8025 let (src_delta, scene_delta) = frontend
8026 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8027 .await
8028 .unwrap();
8029 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8030 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8031 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8032 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8033 assert_eq!(scene_object.id.0, i);
8034 }
8035
8036 let point_id = *scene_delta.new_objects.last().unwrap();
8037
8038 let point_ctor = PointCtor {
8039 position: Point2d {
8040 x: Expr::Number(Number {
8041 value: 3.0,
8042 units: NumericSuffix::Inch,
8043 }),
8044 y: Expr::Number(Number {
8045 value: 4.0,
8046 units: NumericSuffix::Inch,
8047 }),
8048 },
8049 };
8050 let segments = vec![ExistingSegmentCtor {
8051 id: point_id,
8052 ctor: SegmentCtor::Point(point_ctor),
8053 }];
8054 let (src_delta, scene_delta) = frontend
8055 .edit_segments(&mock_ctx, version, sketch_id, segments)
8056 .await
8057 .unwrap();
8058 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8059 assert_eq!(scene_delta.new_objects, vec![]);
8060 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8061
8062 ctx.close().await;
8063 mock_ctx.close().await;
8064 }
8065
8066 #[tokio::test(flavor = "multi_thread")]
8067 async fn test_new_sketch_add_line_edit_line() {
8068 let program = Program::empty();
8069
8070 let mut frontend = FrontendState::new();
8071 frontend.program = program;
8072
8073 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8074 let mock_ctx = ExecutorContext::new_mock(None).await;
8075 let version = Version(0);
8076
8077 let sketch_args = SketchCtor {
8078 on: Plane::Default(PlaneName::Xy),
8079 };
8080 let (_src_delta, scene_delta, sketch_id) = frontend
8081 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8082 .await
8083 .unwrap();
8084 assert_eq!(sketch_id, ObjectId(1));
8085 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8086 let sketch_object = &scene_delta.new_graph.objects[1];
8087 assert_eq!(sketch_object.id, ObjectId(1));
8088 assert_eq!(
8089 sketch_object.kind,
8090 ObjectKind::Sketch(Sketch {
8091 args: SketchCtor {
8092 on: Plane::Default(PlaneName::Xy)
8093 },
8094 plane: ObjectId(0),
8095 segments: vec![],
8096 constraints: vec![],
8097 })
8098 );
8099 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8100
8101 let line_ctor = LineCtor {
8102 start: Point2d {
8103 x: Expr::Number(Number {
8104 value: 0.0,
8105 units: NumericSuffix::Mm,
8106 }),
8107 y: Expr::Number(Number {
8108 value: 0.0,
8109 units: NumericSuffix::Mm,
8110 }),
8111 },
8112 end: Point2d {
8113 x: Expr::Number(Number {
8114 value: 10.0,
8115 units: NumericSuffix::Mm,
8116 }),
8117 y: Expr::Number(Number {
8118 value: 10.0,
8119 units: NumericSuffix::Mm,
8120 }),
8121 },
8122 construction: None,
8123 };
8124 let segment = SegmentCtor::Line(line_ctor);
8125 let (src_delta, scene_delta) = frontend
8126 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8127 .await
8128 .unwrap();
8129 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8130 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8131 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8132 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8133 assert_eq!(scene_object.id.0, i);
8134 }
8135
8136 let line = *scene_delta.new_objects.last().unwrap();
8138
8139 let line_ctor = LineCtor {
8140 start: Point2d {
8141 x: Expr::Number(Number {
8142 value: 1.0,
8143 units: NumericSuffix::Mm,
8144 }),
8145 y: Expr::Number(Number {
8146 value: 2.0,
8147 units: NumericSuffix::Mm,
8148 }),
8149 },
8150 end: Point2d {
8151 x: Expr::Number(Number {
8152 value: 13.0,
8153 units: NumericSuffix::Mm,
8154 }),
8155 y: Expr::Number(Number {
8156 value: 14.0,
8157 units: NumericSuffix::Mm,
8158 }),
8159 },
8160 construction: None,
8161 };
8162 let segments = vec![ExistingSegmentCtor {
8163 id: line,
8164 ctor: SegmentCtor::Line(line_ctor),
8165 }];
8166 let (src_delta, scene_delta) = frontend
8167 .edit_segments(&mock_ctx, version, sketch_id, segments)
8168 .await
8169 .unwrap();
8170 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8171 assert_eq!(scene_delta.new_objects, vec![]);
8172 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8173
8174 ctx.close().await;
8175 mock_ctx.close().await;
8176 }
8177
8178 #[tokio::test(flavor = "multi_thread")]
8179 async fn test_new_sketch_add_arc_edit_arc() {
8180 let program = Program::empty();
8181
8182 let mut frontend = FrontendState::new();
8183 frontend.program = program;
8184
8185 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8186 let mock_ctx = ExecutorContext::new_mock(None).await;
8187 let version = Version(0);
8188
8189 let sketch_args = SketchCtor {
8190 on: Plane::Default(PlaneName::Xy),
8191 };
8192 let (_src_delta, scene_delta, sketch_id) = frontend
8193 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8194 .await
8195 .unwrap();
8196 assert_eq!(sketch_id, ObjectId(1));
8197 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8198 let sketch_object = &scene_delta.new_graph.objects[1];
8199 assert_eq!(sketch_object.id, ObjectId(1));
8200 assert_eq!(
8201 sketch_object.kind,
8202 ObjectKind::Sketch(Sketch {
8203 args: SketchCtor {
8204 on: Plane::Default(PlaneName::Xy),
8205 },
8206 plane: ObjectId(0),
8207 segments: vec![],
8208 constraints: vec![],
8209 })
8210 );
8211 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8212
8213 let arc_ctor = ArcCtor {
8214 start: Point2d {
8215 x: Expr::Var(Number {
8216 value: 0.0,
8217 units: NumericSuffix::Mm,
8218 }),
8219 y: Expr::Var(Number {
8220 value: 0.0,
8221 units: NumericSuffix::Mm,
8222 }),
8223 },
8224 end: Point2d {
8225 x: Expr::Var(Number {
8226 value: 10.0,
8227 units: NumericSuffix::Mm,
8228 }),
8229 y: Expr::Var(Number {
8230 value: 10.0,
8231 units: NumericSuffix::Mm,
8232 }),
8233 },
8234 center: Point2d {
8235 x: Expr::Var(Number {
8236 value: 10.0,
8237 units: NumericSuffix::Mm,
8238 }),
8239 y: Expr::Var(Number {
8240 value: 0.0,
8241 units: NumericSuffix::Mm,
8242 }),
8243 },
8244 direction: None,
8245 construction: None,
8246 };
8247 let segment = SegmentCtor::Arc(arc_ctor);
8248 let (src_delta, scene_delta) = frontend
8249 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8250 .await
8251 .unwrap();
8252 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8253 assert_eq!(
8254 scene_delta.new_objects,
8255 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8256 );
8257 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8258 assert_eq!(scene_object.id.0, i);
8259 }
8260 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8261
8262 let arc = *scene_delta.new_objects.last().unwrap();
8264
8265 let arc_ctor = ArcCtor {
8266 start: Point2d {
8267 x: Expr::Var(Number {
8268 value: 1.0,
8269 units: NumericSuffix::Mm,
8270 }),
8271 y: Expr::Var(Number {
8272 value: 2.0,
8273 units: NumericSuffix::Mm,
8274 }),
8275 },
8276 end: Point2d {
8277 x: Expr::Var(Number {
8278 value: 13.0,
8279 units: NumericSuffix::Mm,
8280 }),
8281 y: Expr::Var(Number {
8282 value: 14.0,
8283 units: NumericSuffix::Mm,
8284 }),
8285 },
8286 center: Point2d {
8287 x: Expr::Var(Number {
8288 value: 13.0,
8289 units: NumericSuffix::Mm,
8290 }),
8291 y: Expr::Var(Number {
8292 value: 2.0,
8293 units: NumericSuffix::Mm,
8294 }),
8295 },
8296 direction: None,
8297 construction: None,
8298 };
8299 let segments = vec![ExistingSegmentCtor {
8300 id: arc,
8301 ctor: SegmentCtor::Arc(arc_ctor),
8302 }];
8303 let (src_delta, scene_delta) = frontend
8304 .edit_segments(&mock_ctx, version, sketch_id, segments)
8305 .await
8306 .unwrap();
8307 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8308 assert_eq!(scene_delta.new_objects, vec![]);
8309 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8310
8311 ctx.close().await;
8312 mock_ctx.close().await;
8313 }
8314
8315 #[tokio::test(flavor = "multi_thread")]
8316 async fn test_new_sketch_add_circle_edit_circle() {
8317 let program = Program::empty();
8318
8319 let mut frontend = FrontendState::new();
8320 frontend.program = program;
8321
8322 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8323 let mock_ctx = ExecutorContext::new_mock(None).await;
8324 let version = Version(0);
8325
8326 let sketch_args = SketchCtor {
8327 on: Plane::Default(PlaneName::Xy),
8328 };
8329 let (_src_delta, _scene_delta, sketch_id) = frontend
8330 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8331 .await
8332 .unwrap();
8333
8334 let circle_ctor = CircleCtor {
8336 start: Point2d {
8337 x: Expr::Var(Number {
8338 value: 5.0,
8339 units: NumericSuffix::Mm,
8340 }),
8341 y: Expr::Var(Number {
8342 value: 0.0,
8343 units: NumericSuffix::Mm,
8344 }),
8345 },
8346 center: Point2d {
8347 x: Expr::Var(Number {
8348 value: 0.0,
8349 units: NumericSuffix::Mm,
8350 }),
8351 y: Expr::Var(Number {
8352 value: 0.0,
8353 units: NumericSuffix::Mm,
8354 }),
8355 },
8356 construction: None,
8357 };
8358 let segment = SegmentCtor::Circle(circle_ctor);
8359 let (src_delta, scene_delta) = frontend
8360 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8361 .await
8362 .unwrap();
8363 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8364 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8366 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8367
8368 let circle = *scene_delta.new_objects.last().unwrap();
8369
8370 let circle_ctor = CircleCtor {
8372 start: Point2d {
8373 x: Expr::Var(Number {
8374 value: 10.0,
8375 units: NumericSuffix::Mm,
8376 }),
8377 y: Expr::Var(Number {
8378 value: 0.0,
8379 units: NumericSuffix::Mm,
8380 }),
8381 },
8382 center: Point2d {
8383 x: Expr::Var(Number {
8384 value: 3.0,
8385 units: NumericSuffix::Mm,
8386 }),
8387 y: Expr::Var(Number {
8388 value: 4.0,
8389 units: NumericSuffix::Mm,
8390 }),
8391 },
8392 construction: None,
8393 };
8394 let segments = vec![ExistingSegmentCtor {
8395 id: circle,
8396 ctor: SegmentCtor::Circle(circle_ctor),
8397 }];
8398 let (src_delta, scene_delta) = frontend
8399 .edit_segments(&mock_ctx, version, sketch_id, segments)
8400 .await
8401 .unwrap();
8402 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8403 assert_eq!(scene_delta.new_objects, vec![]);
8404 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8405
8406 ctx.close().await;
8407 mock_ctx.close().await;
8408 }
8409
8410 #[tokio::test(flavor = "multi_thread")]
8411 async fn test_delete_circle() {
8412 let initial_source = "sketch001 = sketch(on = XY) {
8413 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8414}
8415";
8416
8417 let program = Program::parse(initial_source).unwrap().0.unwrap();
8418 let mut frontend = FrontendState::new();
8419
8420 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8421 let mock_ctx = ExecutorContext::new_mock(None).await;
8422 let version = Version(0);
8423
8424 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8425 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8426 let sketch_id = sketch_object.id;
8427 let sketch = expect_sketch(sketch_object);
8428
8429 assert_eq!(sketch.segments.len(), 3);
8431 let circle_id = sketch.segments[2];
8432
8433 let (src_delta, scene_delta) = frontend
8435 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8436 .await
8437 .unwrap();
8438 insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8439 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8440 let new_sketch = expect_sketch(new_sketch_object);
8441 assert_eq!(new_sketch.segments.len(), 0);
8442
8443 ctx.close().await;
8444 mock_ctx.close().await;
8445 }
8446
8447 #[tokio::test(flavor = "multi_thread")]
8448 async fn test_edit_circle_via_point() {
8449 let initial_source = "sketch001 = sketch(on = XY) {
8450 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8451}
8452";
8453
8454 let program = Program::parse(initial_source).unwrap().0.unwrap();
8455 let mut frontend = FrontendState::new();
8456
8457 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8458 let mock_ctx = ExecutorContext::new_mock(None).await;
8459 let version = Version(0);
8460
8461 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8462 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8463 let sketch_id = sketch_object.id;
8464 let sketch = expect_sketch(sketch_object);
8465
8466 let circle_id = sketch
8468 .segments
8469 .iter()
8470 .copied()
8471 .find(|seg_id| {
8472 matches!(
8473 &frontend.scene_graph.objects[seg_id.0].kind,
8474 ObjectKind::Segment {
8475 segment: Segment::Circle(_)
8476 }
8477 )
8478 })
8479 .expect("Expected a circle segment in sketch");
8480 let circle_object = &frontend.scene_graph.objects[circle_id.0];
8481 let ObjectKind::Segment {
8482 segment: Segment::Circle(circle),
8483 } = &circle_object.kind
8484 else {
8485 panic!("Expected circle segment, got: {:?}", circle_object.kind);
8486 };
8487 let start_point_id = circle.start;
8488
8489 let segments = vec![ExistingSegmentCtor {
8491 id: start_point_id,
8492 ctor: SegmentCtor::Point(PointCtor {
8493 position: Point2d {
8494 x: Expr::Var(Number {
8495 value: 7.0,
8496 units: NumericSuffix::Mm,
8497 }),
8498 y: Expr::Var(Number {
8499 value: 1.0,
8500 units: NumericSuffix::Mm,
8501 }),
8502 },
8503 }),
8504 }];
8505 let (src_delta, _scene_delta) = frontend
8506 .edit_segments(&mock_ctx, version, sketch_id, segments)
8507 .await
8508 .unwrap();
8509 insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8510
8511 ctx.close().await;
8512 mock_ctx.close().await;
8513 }
8514
8515 #[tokio::test(flavor = "multi_thread")]
8516 async fn test_add_line_when_sketch_block_uses_variable() {
8517 let initial_source = "s = sketch(on = XY) {}
8518";
8519
8520 let program = Program::parse(initial_source).unwrap().0.unwrap();
8521
8522 let mut frontend = FrontendState::new();
8523
8524 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8525 let mock_ctx = ExecutorContext::new_mock(None).await;
8526 let version = Version(0);
8527
8528 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8529 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8530 let sketch_id = sketch_object.id;
8531
8532 let line_ctor = LineCtor {
8533 start: Point2d {
8534 x: Expr::Number(Number {
8535 value: 0.0,
8536 units: NumericSuffix::Mm,
8537 }),
8538 y: Expr::Number(Number {
8539 value: 0.0,
8540 units: NumericSuffix::Mm,
8541 }),
8542 },
8543 end: Point2d {
8544 x: Expr::Number(Number {
8545 value: 10.0,
8546 units: NumericSuffix::Mm,
8547 }),
8548 y: Expr::Number(Number {
8549 value: 10.0,
8550 units: NumericSuffix::Mm,
8551 }),
8552 },
8553 construction: None,
8554 };
8555 let segment = SegmentCtor::Line(line_ctor);
8556 let (src_delta, scene_delta) = frontend
8557 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8558 .await
8559 .unwrap();
8560 insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8561 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8562 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8563
8564 ctx.close().await;
8565 mock_ctx.close().await;
8566 }
8567
8568 #[tokio::test(flavor = "multi_thread")]
8569 async fn test_new_sketch_add_line_delete_sketch() {
8570 let program = Program::empty();
8571
8572 let mut frontend = FrontendState::new();
8573 frontend.program = program;
8574
8575 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8576 let mock_ctx = ExecutorContext::new_mock(None).await;
8577 let version = Version(0);
8578
8579 let sketch_args = SketchCtor {
8580 on: Plane::Default(PlaneName::Xy),
8581 };
8582 let (_src_delta, scene_delta, sketch_id) = frontend
8583 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8584 .await
8585 .unwrap();
8586 assert_eq!(sketch_id, ObjectId(1));
8587 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8588 let sketch_object = &scene_delta.new_graph.objects[1];
8589 assert_eq!(sketch_object.id, ObjectId(1));
8590 assert_eq!(
8591 sketch_object.kind,
8592 ObjectKind::Sketch(Sketch {
8593 args: SketchCtor {
8594 on: Plane::Default(PlaneName::Xy)
8595 },
8596 plane: ObjectId(0),
8597 segments: vec![],
8598 constraints: vec![],
8599 })
8600 );
8601 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8602
8603 let line_ctor = LineCtor {
8604 start: Point2d {
8605 x: Expr::Number(Number {
8606 value: 0.0,
8607 units: NumericSuffix::Mm,
8608 }),
8609 y: Expr::Number(Number {
8610 value: 0.0,
8611 units: NumericSuffix::Mm,
8612 }),
8613 },
8614 end: Point2d {
8615 x: Expr::Number(Number {
8616 value: 10.0,
8617 units: NumericSuffix::Mm,
8618 }),
8619 y: Expr::Number(Number {
8620 value: 10.0,
8621 units: NumericSuffix::Mm,
8622 }),
8623 },
8624 construction: None,
8625 };
8626 let segment = SegmentCtor::Line(line_ctor);
8627 let (src_delta, scene_delta) = frontend
8628 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8629 .await
8630 .unwrap();
8631 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8632 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8633
8634 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8635 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8636 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8637
8638 ctx.close().await;
8639 mock_ctx.close().await;
8640 }
8641
8642 #[tokio::test(flavor = "multi_thread")]
8643 async fn test_delete_sketch_when_sketch_block_uses_variable() {
8644 let initial_source = "s = sketch(on = XY) {}
8645";
8646
8647 let program = Program::parse(initial_source).unwrap().0.unwrap();
8648
8649 let mut frontend = FrontendState::new();
8650
8651 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8652 let version = Version(0);
8653
8654 frontend.hack_set_program(&ctx, program).await.unwrap();
8655 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8656 let sketch_id = sketch_object.id;
8657
8658 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8659 insta::assert_snapshot!(
8660 "test_delete_sketch_when_sketch_block_uses_variable",
8661 src_delta.text.as_str()
8662 );
8663 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8664
8665 ctx.close().await;
8666 }
8667
8668 #[tokio::test(flavor = "multi_thread")]
8669 async fn test_delete_sketch_after_comment() {
8670 let initial_source = "sketch001 = sketch(on = XZ) {
8671}
8672";
8673
8674 let program = Program::parse(initial_source).unwrap().0.unwrap();
8675 let mut frontend = FrontendState::new();
8676
8677 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8678 let version = Version(0);
8679
8680 frontend.hack_set_program(&ctx, program).await.unwrap();
8681 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8682 let sketch_id = sketch_object.id;
8683 let original_source = sketch_object.source.clone();
8684
8685 let commented_source = "// test 1
8686sketch001 = sketch(on = XZ) {
8687}
8688";
8689 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8690 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8691
8692 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8693 assert_eq!(cached_sketch_object.source, original_source);
8694
8695 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8696 assert!(
8697 !src_delta.text.contains("sketch001"),
8698 "sketch was not deleted: {}",
8699 src_delta.text
8700 );
8701 insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8703 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8704
8705 ctx.close().await;
8706 }
8707
8708 #[tokio::test(flavor = "multi_thread")]
8709 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8710 let initial_source = "sketch001 = sketch(on = XZ) {
8711}
8712foo = 1
8713";
8714
8715 let program = Program::parse(initial_source).unwrap().0.unwrap();
8716 let mut frontend = FrontendState::new();
8717
8718 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8719 let version = Version(0);
8720
8721 frontend.hack_set_program(&ctx, program).await.unwrap();
8722 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8723 let sketch_id = sketch_object.id;
8724
8725 let commented_source = "// keep me
8726sketch001 = sketch(on = XZ) {
8727}
8728foo = 1
8729";
8730 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8731 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8732
8733 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8734 insta::assert_snapshot!(
8736 "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8737 src_delta.text.as_str()
8738 );
8739
8740 ctx.close().await;
8741 }
8742
8743 #[tokio::test(flavor = "multi_thread")]
8744 async fn test_delete_segment_preserves_pre_comment() {
8745 let initial_source = "\
8746sketch(on = XY) {
8747 point(at = [var 1, var 2])
8748 // describe the middle point
8749 point(at = [var 3, var 4])
8750 point(at = [var 5, var 6])
8751}
8752";
8753
8754 let program = Program::parse(initial_source).unwrap().0.unwrap();
8755 let mut frontend = FrontendState::new();
8756
8757 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8758 let mock_ctx = ExecutorContext::new_mock(None).await;
8759 let version = Version(0);
8760
8761 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8762 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8763 let sketch_id = sketch_object.id;
8764 let sketch = expect_sketch(sketch_object);
8765
8766 let middle_point_id = *sketch.segments.get(1).unwrap();
8767
8768 let (src_delta, _scene_delta) = frontend
8769 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8770 .await
8771 .unwrap();
8772 insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8775
8776 ctx.close().await;
8777 mock_ctx.close().await;
8778 }
8779
8780 #[tokio::test(flavor = "multi_thread")]
8781 async fn test_delete_last_segment_preserves_pre_comment() {
8782 let initial_source = "\
8783sketch(on = XY) {
8784 point(at = [var 1, var 2])
8785 // describe the trailing point
8786 point(at = [var 3, var 4])
8787}
8788";
8789
8790 let program = Program::parse(initial_source).unwrap().0.unwrap();
8791 let mut frontend = FrontendState::new();
8792
8793 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8794 let mock_ctx = ExecutorContext::new_mock(None).await;
8795 let version = Version(0);
8796
8797 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8798 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8799 let sketch_id = sketch_object.id;
8800 let sketch = expect_sketch(sketch_object);
8801
8802 let last_point_id = *sketch.segments.last().unwrap();
8803
8804 let (src_delta, _scene_delta) = frontend
8805 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8806 .await
8807 .unwrap();
8808 insta::assert_snapshot!(
8811 "test_delete_last_segment_preserves_pre_comment",
8812 src_delta.text.as_str()
8813 );
8814
8815 ctx.close().await;
8816 mock_ctx.close().await;
8817 }
8818
8819 #[tokio::test(flavor = "multi_thread")]
8820 async fn test_delete_segment_drops_inline_trailing_comment() {
8821 let initial_source = "\
8822sketch(on = XY) {
8823 point(at = [var 1, var 2])
8824 point(at = [var 3, var 4]) // same-line note that gets dropped
8825 point(at = [var 5, var 6])
8826}
8827";
8828
8829 let program = Program::parse(initial_source).unwrap().0.unwrap();
8830 let mut frontend = FrontendState::new();
8831
8832 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8833 let mock_ctx = ExecutorContext::new_mock(None).await;
8834 let version = Version(0);
8835
8836 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8837 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8838 let sketch_id = sketch_object.id;
8839 let sketch = expect_sketch(sketch_object);
8840
8841 let middle_point_id = *sketch.segments.get(1).unwrap();
8842
8843 let (src_delta, _scene_delta) = frontend
8844 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8845 .await
8846 .unwrap();
8847 assert!(
8849 !src_delta.text.contains("same-line note"),
8850 "inline comment should have been removed: {}",
8851 src_delta.text
8852 );
8853
8854 ctx.close().await;
8855 mock_ctx.close().await;
8856 }
8857
8858 #[tokio::test(flavor = "multi_thread")]
8859 async fn test_delete_segments_preserves_block_comments_across_positions() {
8860 let initial_source = "\
8868sketch(on = XY) {
8869 /* above first - moves to middle */
8870 point(at = [var 1, var 2]) /* same-line on first - dropped */
8871 /* above middle - stays */
8872 point(at = [var 3, var 4])
8873 /* above last - moves to trailing meta */
8874 point(at = [var 5, var 6])
8875}
8876";
8877
8878 let program = Program::parse(initial_source).unwrap().0.unwrap();
8879 let mut frontend = FrontendState::new();
8880
8881 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8882 let mock_ctx = ExecutorContext::new_mock(None).await;
8883 let version = Version(0);
8884
8885 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8886 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8887 let sketch_id = sketch_object.id;
8888 let sketch = expect_sketch(sketch_object);
8889
8890 let first_point_id = *sketch.segments.first().unwrap();
8891 let last_point_id = *sketch.segments.last().unwrap();
8892
8893 let (src_delta, _scene_delta) = frontend
8894 .delete_objects(
8895 &mock_ctx,
8896 version,
8897 sketch_id,
8898 Vec::new(),
8899 vec![first_point_id, last_point_id],
8900 )
8901 .await
8902 .unwrap();
8903 insta::assert_snapshot!(
8904 "test_delete_segments_preserves_block_comments_across_positions",
8905 src_delta.text.as_str()
8906 );
8907
8908 ctx.close().await;
8909 mock_ctx.close().await;
8910 }
8911
8912 #[tokio::test(flavor = "multi_thread")]
8913 async fn test_edit_line_when_editing_its_start_point() {
8914 let initial_source = "\
8915sketch(on = XY) {
8916 line(start = [var 1, var 2], end = [var 3, var 4])
8917}
8918";
8919
8920 let program = Program::parse(initial_source).unwrap().0.unwrap();
8921
8922 let mut frontend = FrontendState::new();
8923
8924 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8925 let mock_ctx = ExecutorContext::new_mock(None).await;
8926 let version = Version(0);
8927
8928 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8929 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8930 let sketch_id = sketch_object.id;
8931 let sketch = expect_sketch(sketch_object);
8932
8933 let point_id = *sketch.segments.first().unwrap();
8934
8935 let point_ctor = PointCtor {
8936 position: Point2d {
8937 x: Expr::Var(Number {
8938 value: 5.0,
8939 units: NumericSuffix::Inch,
8940 }),
8941 y: Expr::Var(Number {
8942 value: 6.0,
8943 units: NumericSuffix::Inch,
8944 }),
8945 },
8946 };
8947 let segments = vec![ExistingSegmentCtor {
8948 id: point_id,
8949 ctor: SegmentCtor::Point(point_ctor),
8950 }];
8951 let (src_delta, scene_delta) = frontend
8952 .edit_segments(&mock_ctx, version, sketch_id, segments)
8953 .await
8954 .unwrap();
8955 insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
8956 assert_eq!(scene_delta.new_objects, vec![]);
8957 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8958
8959 ctx.close().await;
8960 mock_ctx.close().await;
8961 }
8962
8963 #[tokio::test(flavor = "multi_thread")]
8964 async fn test_edit_line_when_editing_its_end_point() {
8965 let initial_source = "\
8966sketch(on = XY) {
8967 line(start = [var 1, var 2], end = [var 3, var 4])
8968}
8969";
8970
8971 let program = Program::parse(initial_source).unwrap().0.unwrap();
8972
8973 let mut frontend = FrontendState::new();
8974
8975 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8976 let mock_ctx = ExecutorContext::new_mock(None).await;
8977 let version = Version(0);
8978
8979 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8980 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8981 let sketch_id = sketch_object.id;
8982 let sketch = expect_sketch(sketch_object);
8983 let point_id = *sketch.segments.get(1).unwrap();
8984
8985 let point_ctor = PointCtor {
8986 position: Point2d {
8987 x: Expr::Var(Number {
8988 value: 5.0,
8989 units: NumericSuffix::Inch,
8990 }),
8991 y: Expr::Var(Number {
8992 value: 6.0,
8993 units: NumericSuffix::Inch,
8994 }),
8995 },
8996 };
8997 let segments = vec![ExistingSegmentCtor {
8998 id: point_id,
8999 ctor: SegmentCtor::Point(point_ctor),
9000 }];
9001 let (src_delta, scene_delta) = frontend
9002 .edit_segments(&mock_ctx, version, sketch_id, segments)
9003 .await
9004 .unwrap();
9005 insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9006 assert_eq!(scene_delta.new_objects, vec![]);
9007 assert_eq!(
9008 scene_delta.new_graph.objects.len(),
9009 5,
9010 "{:#?}",
9011 scene_delta.new_graph.objects
9012 );
9013
9014 ctx.close().await;
9015 mock_ctx.close().await;
9016 }
9017
9018 #[tokio::test(flavor = "multi_thread")]
9019 async fn test_edit_line_with_coincident_feedback() {
9020 let initial_source = "\
9021sketch(on = XY) {
9022 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9023 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9024 fixed([line1.start, [0, 0]])
9025 coincident([line1.end, line2.start])
9026 equalLength([line1, line2])
9027}
9028";
9029
9030 let program = Program::parse(initial_source).unwrap().0.unwrap();
9031
9032 let mut frontend = FrontendState::new();
9033
9034 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9035 let mock_ctx = ExecutorContext::new_mock(None).await;
9036 let version = Version(0);
9037
9038 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9039 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9040 let sketch_id = sketch_object.id;
9041 let sketch = expect_sketch(sketch_object);
9042 let line2_end_id = *sketch.segments.get(4).unwrap();
9043
9044 let segments = vec![ExistingSegmentCtor {
9045 id: line2_end_id,
9046 ctor: SegmentCtor::Point(PointCtor {
9047 position: Point2d {
9048 x: Expr::Var(Number {
9049 value: 9.0,
9050 units: NumericSuffix::None,
9051 }),
9052 y: Expr::Var(Number {
9053 value: 10.0,
9054 units: NumericSuffix::None,
9055 }),
9056 },
9057 }),
9058 }];
9059 let (src_delta, scene_delta) = frontend
9060 .edit_segments(&mock_ctx, version, sketch_id, segments)
9061 .await
9062 .unwrap();
9063 insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9064 assert_eq!(
9065 scene_delta.new_graph.objects.len(),
9066 11,
9067 "{:#?}",
9068 scene_delta.new_graph.objects
9069 );
9070
9071 ctx.close().await;
9072 mock_ctx.close().await;
9073 }
9074
9075 #[tokio::test(flavor = "multi_thread")]
9076 async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9077 let initial_source = "\
9078sketch(on = XY) {
9079 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9080 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9081 fixed([line1.start, [0, 0]])
9082 coincident([line1.end, line2.start])
9083 equalLength([line1, line2])
9084}
9085";
9086
9087 let program = Program::parse(initial_source).unwrap().0.unwrap();
9088 let mut frontend = FrontendState::new();
9089 let mock_ctx = ExecutorContext::new_mock(None).await;
9090 let version = Version(0);
9091
9092 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9093 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9094 let sketch_id = sketch_object.id;
9095 let sketch = expect_sketch(sketch_object);
9096 let line2_end_id = *sketch.segments.get(4).unwrap();
9097
9098 let segments = vec![ExistingSegmentCtor {
9099 id: line2_end_id,
9100 ctor: SegmentCtor::Point(PointCtor {
9101 position: Point2d {
9102 x: Expr::Var(Number {
9103 value: 9.0,
9104 units: NumericSuffix::None,
9105 }),
9106 y: Expr::Var(Number {
9107 value: 10.0,
9108 units: NumericSuffix::None,
9109 }),
9110 },
9111 }),
9112 }];
9113 let (edited_source, _) = frontend
9114 .edit_segments(&mock_ctx, version, sketch_id, segments)
9115 .await
9116 .unwrap();
9117
9118 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9119 assert_eq!(mock_source.text, edited_source.text);
9120
9121 mock_ctx.close().await;
9122 }
9123
9124 #[tokio::test(flavor = "multi_thread")]
9127 async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9128 let initial_source = "\
9129sketch(on = XY) {
9130 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9131 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9132 fixed([line1.start, [0, 0]])
9133 coincident([line1.end, line2.start])
9134 equalLength([line1, line2])
9135}
9136";
9137
9138 let program = Program::parse(initial_source).unwrap().0.unwrap();
9139 let mut frontend = FrontendState::new();
9140 let mock_ctx = ExecutorContext::new_mock(None).await;
9141 let version = Version(0);
9142
9143 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9144 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9145 let sketch_id = sketch_object.id;
9146 let sketch = expect_sketch(sketch_object);
9147 let line2_end_id = *sketch.segments.get(4).unwrap();
9148
9149 let segments = vec![ExistingSegmentCtor {
9150 id: line2_end_id,
9151 ctor: SegmentCtor::Point(PointCtor {
9152 position: Point2d {
9153 x: Expr::Var(Number {
9154 value: 9.0,
9155 units: NumericSuffix::None,
9156 }),
9157 y: Expr::Var(Number {
9158 value: 10.0,
9159 units: NumericSuffix::None,
9160 }),
9161 },
9162 }),
9163 }];
9164 let (preview_source, preview_delta) = frontend
9165 .edit_segments_with_options(
9166 &mock_ctx,
9167 version,
9168 sketch_id,
9169 segments,
9170 EditSegmentsOptions {
9171 anchor_segment_ids: Some(vec![line2_end_id]),
9172 drag_anchors: Vec::new(),
9173 constraint_label_edits: Vec::new(),
9174 commit_solved_initial_guesses: false,
9175 },
9176 )
9177 .await
9178 .unwrap();
9179
9180 assert!(
9181 !preview_delta.exec_outcome.var_solutions.is_empty(),
9182 "preview solve should still solve and return geometry feedback"
9183 );
9184 assert!(
9185 preview_source
9186 .text
9187 .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9188 );
9189 assert!(
9190 preview_source
9191 .text
9192 .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9193 );
9194
9195 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9196 assert_eq!(mock_source.text, preview_source.text);
9197
9198 mock_ctx.close().await;
9199 }
9200
9201 #[tokio::test(flavor = "multi_thread")]
9202 async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9203 let initial_source = "\
9204sketch(on = XY) {
9205 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9206}
9207";
9208
9209 let program = Program::parse(initial_source).unwrap().0.unwrap();
9210 let mut frontend = FrontendState::new();
9211 let mock_ctx = ExecutorContext::new_mock(None).await;
9212 let version = Version(0);
9213
9214 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9215 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9216 let sketch_id = sketch_object.id;
9217 let sketch = expect_sketch(sketch_object);
9218 let line_end_id = *sketch.segments.get(1).unwrap();
9219
9220 let constraint = Constraint::Fixed(Fixed {
9221 points: vec![FixedPoint {
9222 point: line_end_id,
9223 position: Point2d {
9224 x: Number {
9225 value: 20.0,
9226 units: NumericSuffix::Mm,
9227 },
9228 y: Number {
9229 value: 0.0,
9230 units: NumericSuffix::Mm,
9231 },
9232 },
9233 }],
9234 });
9235 let (constraint_source, _) = frontend
9236 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9237 .await
9238 .unwrap();
9239
9240 assert!(
9241 constraint_source
9242 .text
9243 .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9244 "{}",
9245 constraint_source.text
9246 );
9247 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9248 assert_eq!(mock_source.text, constraint_source.text);
9249
9250 mock_ctx.close().await;
9251 }
9252
9253 #[test]
9254 fn test_no_solver_feedback_preserves_original_source() {
9255 let initial_source = "\
9256@settings(defaultLengthUnit = in, kclVersion = 2.0)
9257cylinder = startSketchOn(XY)
9258 |> circle(center= [0, 0], radius= 22)
9259 |> extrude(length = 14)
9260";
9261 let mut frontend = FrontendState::new();
9262 frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9263 let outcome = ExecOutcome {
9264 variables: Default::default(),
9265 operations: Default::default(),
9266 artifact_graph: Default::default(),
9267 scene_objects: Default::default(),
9268 source_range_to_object: Default::default(),
9269 var_solutions: Default::default(),
9270 refactor_metadata: Default::default(),
9271 issues: Default::default(),
9272 filenames: Default::default(),
9273 source_files: Default::default(),
9274 default_planes: Default::default(),
9275 };
9276
9277 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9278
9279 assert_eq!(source_delta.text, initial_source);
9280 }
9281
9282 #[tokio::test(flavor = "multi_thread")]
9285 async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9286 let initial_source = "\
9287sketch(on = XY) {
9288 point1 = point(at = [var 0mm, var 0mm])
9289 point2 = point(at = [var 0mm, var 0mm])
9290 coincident([point1, point2])
9291}
9292";
9293
9294 let program = Program::parse(initial_source).unwrap().0.unwrap();
9295 let mut frontend = FrontendState::new();
9296 let mock_ctx = ExecutorContext::new_mock(None).await;
9297 let version = Version(0);
9298
9299 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9300 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9301 let sketch_id = sketch_object.id;
9302 let sketch = expect_sketch(sketch_object);
9303 let point1_id = sketch.segments[0];
9304 let point2_id = sketch.segments[1];
9305
9306 let segments = vec![
9307 ExistingSegmentCtor {
9308 id: point1_id,
9309 ctor: SegmentCtor::Point(PointCtor {
9310 position: point_expr_mm(10.0, 0.0),
9311 }),
9312 },
9313 ExistingSegmentCtor {
9314 id: point2_id,
9315 ctor: SegmentCtor::Point(PointCtor {
9316 position: point_expr_mm(100.0, 0.0),
9317 }),
9318 },
9319 ];
9320 let (_, scene_delta) = frontend
9321 .edit_segments_with_options(
9322 &mock_ctx,
9323 version,
9324 sketch_id,
9325 segments,
9326 EditSegmentsOptions {
9327 anchor_segment_ids: Some(vec![point1_id]),
9328 drag_anchors: Vec::new(),
9329 constraint_label_edits: Vec::new(),
9330 commit_solved_initial_guesses: true,
9331 },
9332 )
9333 .await
9334 .unwrap();
9335
9336 assert_point_position_close(
9337 point_position(&scene_delta.new_graph, point1_id),
9338 point_number_mm(10.0, 0.0),
9339 );
9340 assert_point_position_close(
9341 point_position(&scene_delta.new_graph, point2_id),
9342 point_number_mm(10.0, 0.0),
9343 );
9344
9345 mock_ctx.close().await;
9346 }
9347
9348 fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9351 use std::cell::RefCell;
9352 struct Collector {
9353 target: f64,
9354 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9355 }
9356 impl<'a> crate::walk::Visitor<'a> for &Collector {
9357 type Error = crate::front::Error;
9358 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9359 if let crate::walk::Node::SketchVar(sketch_var) = node
9360 && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9361 && (initial.value - self.target).abs() < 1e-9
9362 {
9363 self.out
9364 .borrow_mut()
9365 .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9366 }
9367 for child in node.children().iter() {
9368 if !child.visit(*self)? {
9369 return Ok(false);
9370 }
9371 }
9372 Ok(true)
9373 }
9374 }
9375 let collector = Collector {
9376 target: value,
9377 out: Default::default(),
9378 };
9379 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9380 collector.out.into_inner()
9381 }
9382
9383 fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9386 use std::cell::RefCell;
9387 struct Collector {
9388 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9389 }
9390 impl<'a> crate::walk::Visitor<'a> for &Collector {
9391 type Error = crate::front::Error;
9392 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9393 if let crate::walk::Node::SketchVar(sketch_var) = node
9394 && let Some(node_path) = &sketch_var.node_path
9395 {
9396 self.out
9397 .borrow_mut()
9398 .push((SourceRange::from(sketch_var), node_path.clone()));
9399 }
9400 for child in node.children().iter() {
9401 if !child.visit(*self)? {
9402 return Ok(false);
9403 }
9404 }
9405 Ok(true)
9406 }
9407 }
9408 let collector = Collector {
9409 out: Default::default(),
9410 };
9411 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9412 collector.out.into_inner()
9413 }
9414
9415 fn empty_exec_outcome_with_var_solutions(
9416 var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9417 ) -> ExecOutcome {
9418 ExecOutcome {
9419 variables: Default::default(),
9420 operations: Default::default(),
9421 artifact_graph: Default::default(),
9422 scene_objects: Default::default(),
9423 source_range_to_object: Default::default(),
9424 var_solutions,
9425 refactor_metadata: Default::default(),
9426 issues: Default::default(),
9427 filenames: Default::default(),
9428 source_files: Default::default(),
9429 default_planes: Default::default(),
9430 }
9431 }
9432
9433 #[test]
9437 fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9438 let initial_source = "\
9439sketch(on = XY) {
9440 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9441}
9442";
9443 let program = Program::parse(initial_source).unwrap().0.unwrap();
9444 let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9445 assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9446 let (literal_range, node_path) = matches.into_iter().next().unwrap();
9447
9448 let mut frontend = FrontendState::new();
9449 frontend.program = program;
9450
9451 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9452 literal_range,
9453 Some(node_path),
9454 Number {
9455 value: 25.0,
9456 units: NumericSuffix::Mm,
9457 },
9458 )]);
9459
9460 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9461
9462 insta::assert_snapshot!(
9463 "test_commit_var_solution_by_node_path_updates_sketch_var",
9464 source_delta.text
9465 );
9466 }
9467
9468 #[test]
9475 fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9476 let compact_source = "\
9477sketch(on = XY) {
9478 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9479}
9480";
9481 let padded_source = "\
9482// added comment\n// added comment\n\nsketch(on = XY) {
9483 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9484}
9485";
9486 let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9487 let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9488
9489 let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9490 .into_iter()
9491 .next()
9492 .expect("expected `var 10mm` in compact source");
9493 let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9494 .into_iter()
9495 .next()
9496 .expect("expected `var 10mm` in padded source");
9497
9498 assert_ne!(
9499 compact_match.0, padded_match.0,
9500 "byte offsets must differ for this test to be meaningful"
9501 );
9502 assert_eq!(
9503 compact_match.1, padded_match.1,
9504 "node paths must agree across whitespace; that's the whole point of NodePath",
9505 );
9506
9507 let mut frontend = FrontendState::new();
9508 frontend.program = padded_program;
9509
9510 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9512 compact_match.0,
9513 Some(compact_match.1),
9514 Number {
9515 value: 30.0,
9516 units: NumericSuffix::Mm,
9517 },
9518 )]);
9519
9520 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9521
9522 insta::assert_snapshot!(
9523 "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9524 source_delta.text
9525 );
9526 }
9527
9528 #[test]
9532 fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9533 let initial_source = "\
9534sketch(on = XY) {
9535 line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9536}
9537";
9538 let program = Program::parse(initial_source).unwrap().0.unwrap();
9539
9540 let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9541 .into_iter()
9542 .next()
9543 .expect("expected `var 10mm`");
9544 let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9545 .into_iter()
9546 .next()
9547 .expect("expected `var 20mm`");
9548
9549 let mut frontend = FrontendState::new();
9550 frontend.program = program;
9551
9552 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9554 var_20.0,
9555 Some(var_10.1),
9556 Number {
9557 value: 33.0,
9558 units: NumericSuffix::Mm,
9559 },
9560 )]);
9561
9562 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9563
9564 insta::assert_snapshot!(
9565 "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9566 source_delta.text
9567 );
9568 }
9569
9570 #[test]
9577 fn test_commit_var_solution_writes_back_into_bare_var() {
9578 let initial_source = "\
9579@settings(experimentalFeatures = allow, kclVersion = 2.0)
9580sketch(on = XY) {
9581 line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9582}
9583";
9584 let program = Program::parse(initial_source).unwrap().0.unwrap();
9585
9586 let bare = collect_all_sketch_vars(&program)
9589 .into_iter()
9590 .find(|(range, _)| {
9591 range.end() - range.start() == 3
9593 })
9594 .expect("expected at least one bare `var`");
9595
9596 let mut frontend = FrontendState::new();
9597 frontend.program = program;
9598
9599 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9600 bare.0,
9601 Some(bare.1),
9602 Number {
9603 value: 7.0,
9604 units: NumericSuffix::Mm,
9605 },
9606 )]);
9607
9608 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9609
9610 insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9614 }
9615
9616 #[tokio::test(flavor = "multi_thread")]
9617 async fn test_delete_point_without_var() {
9618 let initial_source = "\
9619sketch(on = XY) {
9620 point(at = [var 1, var 2])
9621 point(at = [var 3, var 4])
9622 point(at = [var 5, var 6])
9623}
9624";
9625
9626 let program = Program::parse(initial_source).unwrap().0.unwrap();
9627
9628 let mut frontend = FrontendState::new();
9629
9630 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9631 let mock_ctx = ExecutorContext::new_mock(None).await;
9632 let version = Version(0);
9633
9634 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9635 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9636 let sketch_id = sketch_object.id;
9637 let sketch = expect_sketch(sketch_object);
9638
9639 let point_id = *sketch.segments.get(1).unwrap();
9640
9641 let (src_delta, scene_delta) = frontend
9642 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9643 .await
9644 .unwrap();
9645 insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9646 assert_eq!(scene_delta.new_objects, vec![]);
9647 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9648
9649 ctx.close().await;
9650 mock_ctx.close().await;
9651 }
9652
9653 #[tokio::test(flavor = "multi_thread")]
9654 async fn test_delete_point_with_var() {
9655 let initial_source = "\
9656sketch(on = XY) {
9657 point(at = [var 1, var 2])
9658 point1 = point(at = [var 3, var 4])
9659 point(at = [var 5, var 6])
9660}
9661";
9662
9663 let program = Program::parse(initial_source).unwrap().0.unwrap();
9664
9665 let mut frontend = FrontendState::new();
9666
9667 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9668 let mock_ctx = ExecutorContext::new_mock(None).await;
9669 let version = Version(0);
9670
9671 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9672 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9673 let sketch_id = sketch_object.id;
9674 let sketch = expect_sketch(sketch_object);
9675
9676 let point_id = *sketch.segments.get(1).unwrap();
9677
9678 let (src_delta, scene_delta) = frontend
9679 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9680 .await
9681 .unwrap();
9682 insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9683 assert_eq!(scene_delta.new_objects, vec![]);
9684 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9685
9686 ctx.close().await;
9687 mock_ctx.close().await;
9688 }
9689
9690 #[tokio::test(flavor = "multi_thread")]
9691 async fn test_delete_multiple_points() {
9692 let initial_source = "\
9693sketch(on = XY) {
9694 point(at = [var 1, var 2])
9695 point1 = point(at = [var 3, var 4])
9696 point(at = [var 5, var 6])
9697}
9698";
9699
9700 let program = Program::parse(initial_source).unwrap().0.unwrap();
9701
9702 let mut frontend = FrontendState::new();
9703
9704 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9705 let mock_ctx = ExecutorContext::new_mock(None).await;
9706 let version = Version(0);
9707
9708 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9709 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9710 let sketch_id = sketch_object.id;
9711
9712 let sketch = expect_sketch(sketch_object);
9713
9714 let point1_id = *sketch.segments.first().unwrap();
9715 let point2_id = *sketch.segments.get(1).unwrap();
9716
9717 let (src_delta, scene_delta) = frontend
9718 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9719 .await
9720 .unwrap();
9721 insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9722 assert_eq!(scene_delta.new_objects, vec![]);
9723 assert_eq!(scene_delta.new_graph.objects.len(), 3);
9724
9725 ctx.close().await;
9726 mock_ctx.close().await;
9727 }
9728
9729 #[tokio::test(flavor = "multi_thread")]
9730 async fn test_delete_coincident_constraint() {
9731 let initial_source = "\
9732sketch(on = XY) {
9733 point1 = point(at = [var 1, var 2])
9734 point2 = point(at = [var 3, var 4])
9735 coincident([point1, point2])
9736 point(at = [var 5, var 6])
9737}
9738";
9739
9740 let program = Program::parse(initial_source).unwrap().0.unwrap();
9741
9742 let mut frontend = FrontendState::new();
9743
9744 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9745 let mock_ctx = ExecutorContext::new_mock(None).await;
9746 let version = Version(0);
9747
9748 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9749 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9750 let sketch_id = sketch_object.id;
9751 let sketch = expect_sketch(sketch_object);
9752
9753 let coincident_id = *sketch.constraints.first().unwrap();
9754
9755 let (src_delta, scene_delta) = frontend
9756 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9757 .await
9758 .unwrap();
9759 insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9760 assert_eq!(scene_delta.new_objects, vec![]);
9761 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9762
9763 ctx.close().await;
9764 mock_ctx.close().await;
9765 }
9766
9767 #[tokio::test(flavor = "multi_thread")]
9768 async fn test_delete_line_cascades_to_coincident_constraint() {
9769 let initial_source = "\
9770sketch(on = XY) {
9771 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9772 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9773 coincident([line1.end, line2.start])
9774}
9775";
9776
9777 let program = Program::parse(initial_source).unwrap().0.unwrap();
9778
9779 let mut frontend = FrontendState::new();
9780
9781 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9782 let mock_ctx = ExecutorContext::new_mock(None).await;
9783 let version = Version(0);
9784
9785 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9786 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9787 let sketch_id = sketch_object.id;
9788 let sketch = expect_sketch(sketch_object);
9789 let line_id = *sketch.segments.get(5).unwrap();
9790
9791 let (src_delta, scene_delta) = frontend
9792 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9793 .await
9794 .unwrap();
9795 insta::assert_snapshot!(
9796 "test_delete_line_cascades_to_coincident_constraint",
9797 src_delta.text.as_str()
9798 );
9799 assert_eq!(
9800 scene_delta.new_graph.objects.len(),
9801 5,
9802 "{:#?}",
9803 scene_delta.new_graph.objects
9804 );
9805
9806 ctx.close().await;
9807 mock_ctx.close().await;
9808 }
9809
9810 #[tokio::test(flavor = "multi_thread")]
9811 async fn test_delete_line_cascades_to_distance_constraint() {
9812 let initial_source = "\
9813sketch(on = XY) {
9814 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9815 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9816 distance([line1.end, line2.start]) == 10mm
9817}
9818";
9819
9820 let program = Program::parse(initial_source).unwrap().0.unwrap();
9821
9822 let mut frontend = FrontendState::new();
9823
9824 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9825 let mock_ctx = ExecutorContext::new_mock(None).await;
9826 let version = Version(0);
9827
9828 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9829 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9830 let sketch_id = sketch_object.id;
9831 let sketch = expect_sketch(sketch_object);
9832 let line_id = *sketch.segments.get(5).unwrap();
9833
9834 let (src_delta, scene_delta) = frontend
9835 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9836 .await
9837 .unwrap();
9838 insta::assert_snapshot!(
9839 "test_delete_line_cascades_to_distance_constraint",
9840 src_delta.text.as_str()
9841 );
9842 assert_eq!(
9843 scene_delta.new_graph.objects.len(),
9844 5,
9845 "{:#?}",
9846 scene_delta.new_graph.objects
9847 );
9848
9849 ctx.close().await;
9850 mock_ctx.close().await;
9851 }
9852
9853 #[tokio::test(flavor = "multi_thread")]
9854 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9855 let initial_source = "\
9856sketch(on = XY) {
9857 point1 = point(at = [var 1, var 2])
9858 point2 = point(at = [var 3, var 4])
9859 horizontalDistance([point1, point2]) == 10mm
9860}
9861";
9862
9863 let program = Program::parse(initial_source).unwrap().0.unwrap();
9864
9865 let mut frontend = FrontendState::new();
9866
9867 let mock_ctx = ExecutorContext::new_mock(None).await;
9868 let version = Version(0);
9869
9870 frontend.program = program.clone();
9871 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9872 frontend.update_state_after_exec(outcome, true);
9873 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9874 let sketch_id = sketch_object.id;
9875 let sketch = expect_sketch(sketch_object);
9876 let point2_id = *sketch.segments.get(1).unwrap();
9877
9878 let (src_delta, scene_delta) = frontend
9879 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9880 .await
9881 .unwrap();
9882 insta::assert_snapshot!(
9883 "test_delete_point_cascades_to_horizontal_distance_constraint",
9884 src_delta.text.as_str()
9885 );
9886 assert_eq!(
9887 scene_delta.new_graph.objects.len(),
9888 3,
9889 "{:#?}",
9890 scene_delta.new_graph.objects
9891 );
9892
9893 mock_ctx.close().await;
9894 }
9895
9896 #[tokio::test(flavor = "multi_thread")]
9897 async fn test_delete_line_cascades_to_fixed_constraint() {
9898 let initial_source = "\
9899sketch(on = XY) {
9900 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9901 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9902 fixed([line1.start, [0, 0]])
9903}
9904";
9905
9906 let program = Program::parse(initial_source).unwrap().0.unwrap();
9907
9908 let mut frontend = FrontendState::new();
9909
9910 let mock_ctx = ExecutorContext::new_mock(None).await;
9911 let version = Version(0);
9912
9913 frontend.program = program.clone();
9914 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9915 frontend.update_state_after_exec(outcome, true);
9916 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9917 let sketch_id = sketch_object.id;
9918 let sketch = expect_sketch(sketch_object);
9919 let line1_id = *sketch.segments.get(2).unwrap();
9920
9921 let (src_delta, scene_delta) = frontend
9922 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9923 .await
9924 .unwrap();
9925 insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9926 assert_eq!(
9927 scene_delta.new_graph.objects.len(),
9928 5,
9929 "{:#?}",
9930 scene_delta.new_graph.objects
9931 );
9932
9933 mock_ctx.close().await;
9934 }
9935
9936 #[tokio::test(flavor = "multi_thread")]
9937 async fn test_delete_line_cascades_to_midpoint_constraint() {
9938 let initial_source = "\
9939sketch(on = XY) {
9940 point1 = point(at = [var 1, var 2])
9941 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9942 midpoint(line1, point = point1)
9943}
9944";
9945
9946 let program = Program::parse(initial_source).unwrap().0.unwrap();
9947
9948 let mut frontend = FrontendState::new();
9949
9950 let mock_ctx = ExecutorContext::new_mock(None).await;
9951 let version = Version(0);
9952
9953 frontend.program = program.clone();
9954 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9955 frontend.update_state_after_exec(outcome, true);
9956 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9957 let sketch_id = sketch_object.id;
9958 let sketch = expect_sketch(sketch_object);
9959 let line1_id = *sketch.segments.get(3).unwrap();
9960
9961 let (src_delta, scene_delta) = frontend
9962 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9963 .await
9964 .unwrap();
9965 insta::assert_snapshot!(
9966 "test_delete_line_cascades_to_midpoint_constraint",
9967 src_delta.text.as_str()
9968 );
9969 assert_eq!(
9970 scene_delta.new_graph.objects.len(),
9971 3,
9972 "{:#?}",
9973 scene_delta.new_graph.objects
9974 );
9975
9976 mock_ctx.close().await;
9977 }
9978
9979 #[tokio::test(flavor = "multi_thread")]
9980 async fn test_delete_point_preserves_multiline_coincident_constraint() {
9981 let initial_source = "\
9982sketch(on = XY) {
9983 point1 = point(at = [var 1, var 2])
9984 point2 = point(at = [var 3, var 4])
9985 point3 = point(at = [var 5, var 6])
9986 coincident([point1, point2, point3])
9987}
9988";
9989
9990 let program = Program::parse(initial_source).unwrap().0.unwrap();
9991
9992 let mut frontend = FrontendState::new();
9993
9994 let mock_ctx = ExecutorContext::new_mock(None).await;
9995 let version = Version(0);
9996
9997 frontend.program = program.clone();
9998 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9999 frontend.update_state_after_exec(outcome, true);
10000 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10001 let sketch_id = sketch_object.id;
10002 let sketch = expect_sketch(sketch_object);
10003 let point3_id = *sketch.segments.get(2).unwrap();
10004
10005 let (src_delta, scene_delta) = frontend
10006 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10007 .await
10008 .unwrap();
10009 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10010 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10011 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10012 assert!(
10013 src_delta.text.contains("coincident([point1, point2])"),
10014 "{}",
10015 src_delta.text
10016 );
10017
10018 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10019 let sketch = expect_sketch(sketch_object);
10020 assert_eq!(sketch.segments.len(), 2);
10021 assert_eq!(sketch.constraints.len(), 1);
10022
10023 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10024 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10025 panic!("Expected constraint object");
10026 };
10027 let Constraint::Coincident(coincident) = constraint else {
10028 panic!("Expected coincident constraint");
10029 };
10030 assert_eq!(
10031 coincident.segments,
10032 sketch
10033 .segments
10034 .iter()
10035 .copied()
10036 .map(Into::into)
10037 .collect::<Vec<ConstraintSegment>>()
10038 );
10039
10040 mock_ctx.close().await;
10041 }
10042
10043 #[tokio::test(flavor = "multi_thread")]
10044 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10045 let initial_source = "\
10046sketch(on = XY) {
10047 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10048 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10049 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10050 equalLength([line1, line2, line3])
10051}
10052";
10053
10054 let program = Program::parse(initial_source).unwrap().0.unwrap();
10055
10056 let mut frontend = FrontendState::new();
10057
10058 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10059 let mock_ctx = ExecutorContext::new_mock(None).await;
10060 let version = Version(0);
10061
10062 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10063 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10064 let sketch_id = sketch_object.id;
10065 let sketch = expect_sketch(sketch_object);
10066 let line3_id = *sketch.segments.get(8).unwrap();
10067
10068 let (src_delta, scene_delta) = frontend
10069 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10070 .await
10071 .unwrap();
10072 insta::assert_snapshot!(
10073 "test_delete_line_preserves_multiline_equal_length_constraint",
10074 src_delta.text.as_str()
10075 );
10076
10077 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10078 let sketch = expect_sketch(sketch_object);
10079 assert_eq!(sketch.constraints.len(), 1);
10080
10081 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10082 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10083 panic!("Expected constraint object");
10084 };
10085 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10086 panic!("Expected lines equal length constraint");
10087 };
10088 assert_eq!(lines_equal_length.lines.len(), 2);
10089
10090 ctx.close().await;
10091 mock_ctx.close().await;
10092 }
10093
10094 #[tokio::test(flavor = "multi_thread")]
10095 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10096 let initial_source = "\
10097sketch(on = XY) {
10098 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10099 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10100 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10101 horizontal([line1.end, line2.start, line3.start])
10102}
10103";
10104
10105 let program = Program::parse(initial_source).unwrap().0.unwrap();
10106
10107 let mut frontend = FrontendState::new();
10108
10109 let mock_ctx = ExecutorContext::new_mock(None).await;
10110 let version = Version(0);
10111
10112 frontend.program = program.clone();
10113 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10114 frontend.update_state_after_exec(outcome, true);
10115 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10116 let sketch_id = sketch_object.id;
10117 let sketch = expect_sketch(sketch_object);
10118 let line1_id = *sketch.segments.get(2).unwrap();
10119
10120 let (src_delta, scene_delta) = frontend
10121 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10122 .await
10123 .unwrap();
10124 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10125 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10126 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10127 assert!(
10128 src_delta.text.contains("horizontal([line2.start, line3.start])"),
10129 "{}",
10130 src_delta.text
10131 );
10132
10133 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10134 let sketch = expect_sketch(sketch_object);
10135 assert_eq!(sketch.constraints.len(), 1);
10136
10137 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10138 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10139 panic!("Expected constraint object");
10140 };
10141 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10142 panic!("Expected horizontal points constraint");
10143 };
10144 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10145 assert_eq!(*points, remaining_points);
10146
10147 mock_ctx.close().await;
10148 }
10149
10150 #[tokio::test(flavor = "multi_thread")]
10151 async fn test_delete_line_preserves_multiline_vertical_constraint() {
10152 let initial_source = "\
10153sketch(on = XY) {
10154 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10155 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10156 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10157 vertical([line1.end, line2.start, line3.start])
10158}
10159";
10160
10161 let program = Program::parse(initial_source).unwrap().0.unwrap();
10162
10163 let mut frontend = FrontendState::new();
10164
10165 let mock_ctx = ExecutorContext::new_mock(None).await;
10166 let version = Version(0);
10167
10168 frontend.program = program.clone();
10169 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10170 frontend.update_state_after_exec(outcome, true);
10171 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10172 let sketch_id = sketch_object.id;
10173 let sketch = expect_sketch(sketch_object);
10174 let line1_id = *sketch.segments.get(2).unwrap();
10175
10176 let (src_delta, scene_delta) = frontend
10177 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10178 .await
10179 .unwrap();
10180 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10181 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10182 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10183 assert!(
10184 src_delta.text.contains("vertical([line2.start, line3.start])"),
10185 "{}",
10186 src_delta.text
10187 );
10188
10189 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10190 let sketch = expect_sketch(sketch_object);
10191 assert_eq!(sketch.constraints.len(), 1);
10192
10193 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10194 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10195 panic!("Expected constraint object");
10196 };
10197 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10198 panic!("Expected vertical points constraint");
10199 };
10200 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10201 assert_eq!(*points, remaining_points);
10202
10203 mock_ctx.close().await;
10204 }
10205
10206 #[tokio::test(flavor = "multi_thread")]
10207 async fn test_delete_line_preserves_multiline_coincident_constraint() {
10208 let initial_source = "\
10209sketch(on = XY) {
10210 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10211 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10212 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10213 coincident([line1.end, line2.start, line3.start])
10214}
10215";
10216
10217 let program = Program::parse(initial_source).unwrap().0.unwrap();
10218
10219 let mut frontend = FrontendState::new();
10220
10221 let mock_ctx = ExecutorContext::new_mock(None).await;
10222 let version = Version(0);
10223
10224 frontend.program = program.clone();
10225 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10226 frontend.update_state_after_exec(outcome, true);
10227 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10228 let sketch_id = sketch_object.id;
10229 let sketch = expect_sketch(sketch_object);
10230 let line1_id = *sketch.segments.get(2).unwrap();
10231
10232 let (src_delta, scene_delta) = frontend
10233 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10234 .await
10235 .unwrap();
10236 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10237 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10238 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10239 assert!(
10240 src_delta.text.contains("coincident([line2.start, line3.start])"),
10241 "{}",
10242 src_delta.text
10243 );
10244
10245 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10246 let sketch = expect_sketch(sketch_object);
10247 assert_eq!(sketch.constraints.len(), 1);
10248
10249 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10250 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10251 panic!("Expected constraint object");
10252 };
10253 let Constraint::Coincident(coincident) = constraint else {
10254 panic!("Expected coincident constraint");
10255 };
10256 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10257 assert_eq!(coincident.segments, remaining_segments);
10258
10259 mock_ctx.close().await;
10260 }
10261
10262 #[tokio::test(flavor = "multi_thread")]
10263 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10264 let initial_source = "\
10265sketch(on = XY) {
10266 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10267 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10268 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10269 equalLength([line1, line2, line3])
10270}
10271";
10272
10273 let program = Program::parse(initial_source).unwrap().0.unwrap();
10274
10275 let mut frontend = FrontendState::new();
10276
10277 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10278 let mock_ctx = ExecutorContext::new_mock(None).await;
10279 let version = Version(0);
10280
10281 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10282 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10283 let sketch_id = sketch_object.id;
10284 let sketch = expect_sketch(sketch_object);
10285 let line2_id = *sketch.segments.get(5).unwrap();
10286 let line3_id = *sketch.segments.get(8).unwrap();
10287
10288 let (src_delta, scene_delta) = frontend
10289 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10290 .await
10291 .unwrap();
10292 insta::assert_snapshot!(
10293 "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10294 src_delta.text.as_str()
10295 );
10296
10297 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10298 let sketch = expect_sketch(sketch_object);
10299 assert!(sketch.constraints.is_empty());
10300
10301 ctx.close().await;
10302 mock_ctx.close().await;
10303 }
10304
10305 #[tokio::test(flavor = "multi_thread")]
10306 async fn test_delete_line_preserves_multiline_parallel_constraint() {
10307 let initial_source = "\
10308sketch(on = XY) {
10309 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10310 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10311 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10312 parallel([line1, line2, line3])
10313}
10314";
10315
10316 let program = Program::parse(initial_source).unwrap().0.unwrap();
10317
10318 let mut frontend = FrontendState::new();
10319
10320 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10321 let mock_ctx = ExecutorContext::new_mock(None).await;
10322 let version = Version(0);
10323
10324 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10325 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10326 let sketch_id = sketch_object.id;
10327 let sketch = expect_sketch(sketch_object);
10328 let line3_id = *sketch.segments.get(8).unwrap();
10329
10330 let (src_delta, scene_delta) = frontend
10331 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10332 .await
10333 .unwrap();
10334 insta::assert_snapshot!(
10335 "test_delete_line_preserves_multiline_parallel_constraint",
10336 src_delta.text.as_str()
10337 );
10338
10339 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10340 let sketch = expect_sketch(sketch_object);
10341 assert_eq!(sketch.constraints.len(), 1);
10342
10343 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10344 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10345 panic!("Expected constraint object");
10346 };
10347 let Constraint::Parallel(parallel) = constraint else {
10348 panic!("Expected parallel constraint");
10349 };
10350 assert_eq!(parallel.lines.len(), 2);
10351
10352 ctx.close().await;
10353 mock_ctx.close().await;
10354 }
10355
10356 #[tokio::test(flavor = "multi_thread")]
10357 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10358 let initial_source = "\
10359sketch(on = XY) {
10360 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10361 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10362 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10363 parallel([line1, line2, line3])
10364}
10365";
10366
10367 let program = Program::parse(initial_source).unwrap().0.unwrap();
10368
10369 let mut frontend = FrontendState::new();
10370
10371 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10372 let mock_ctx = ExecutorContext::new_mock(None).await;
10373 let version = Version(0);
10374
10375 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10376 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10377 let sketch_id = sketch_object.id;
10378 let sketch = expect_sketch(sketch_object);
10379 let line2_id = *sketch.segments.get(5).unwrap();
10380 let line3_id = *sketch.segments.get(8).unwrap();
10381
10382 let (src_delta, scene_delta) = frontend
10383 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10384 .await
10385 .unwrap();
10386 insta::assert_snapshot!(
10387 "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10388 src_delta.text.as_str()
10389 );
10390
10391 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10392 let sketch = expect_sketch(sketch_object);
10393 assert!(sketch.constraints.is_empty());
10394
10395 ctx.close().await;
10396 mock_ctx.close().await;
10397 }
10398
10399 #[tokio::test(flavor = "multi_thread")]
10400 async fn test_delete_line_line_coincident_constraint() {
10401 let initial_source = "\
10402sketch(on = XY) {
10403 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10404 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10405 coincident([line1, line2])
10406}
10407";
10408
10409 let program = Program::parse(initial_source).unwrap().0.unwrap();
10410
10411 let mut frontend = FrontendState::new();
10412
10413 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10414 let mock_ctx = ExecutorContext::new_mock(None).await;
10415 let version = Version(0);
10416
10417 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10418 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10419 let sketch_id = sketch_object.id;
10420 let sketch = expect_sketch(sketch_object);
10421
10422 let coincident_id = *sketch.constraints.first().unwrap();
10423
10424 let (src_delta, scene_delta) = frontend
10425 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10426 .await
10427 .unwrap();
10428 insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10429 assert_eq!(scene_delta.new_objects, vec![]);
10430 assert_eq!(scene_delta.new_graph.objects.len(), 8);
10431
10432 ctx.close().await;
10433 mock_ctx.close().await;
10434 }
10435
10436 #[tokio::test(flavor = "multi_thread")]
10437 async fn test_two_points_coincident() {
10438 let initial_source = "\
10439sketch(on = XY) {
10440 point1 = point(at = [var 1, var 2])
10441 point(at = [3, 4])
10442}
10443";
10444
10445 let program = Program::parse(initial_source).unwrap().0.unwrap();
10446
10447 let mut frontend = FrontendState::new();
10448
10449 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10450 let mock_ctx = ExecutorContext::new_mock(None).await;
10451 let version = Version(0);
10452
10453 frontend.hack_set_program(&ctx, program).await.unwrap();
10454 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10455 let sketch_id = sketch_object.id;
10456 let sketch = expect_sketch(sketch_object);
10457 let point0_id = *sketch.segments.first().unwrap();
10458 let point1_id = *sketch.segments.get(1).unwrap();
10459
10460 let constraint = Constraint::Coincident(Coincident {
10461 segments: vec![point0_id.into(), point1_id.into()],
10462 });
10463 let (src_delta, scene_delta) = frontend
10464 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10465 .await
10466 .unwrap();
10467 insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10468 assert_eq!(
10469 scene_delta.new_graph.objects.len(),
10470 5,
10471 "{:#?}",
10472 scene_delta.new_graph.objects
10473 );
10474
10475 ctx.close().await;
10476 mock_ctx.close().await;
10477 }
10478
10479 #[tokio::test(flavor = "multi_thread")]
10480 async fn test_three_points_coincident() {
10481 let initial_source = "\
10482sketch(on = XY) {
10483 point1 = point(at = [var 1, var 2])
10484 point(at = [var 3, var 4])
10485 point(at = [var 5, var 6])
10486}
10487";
10488
10489 let program = Program::parse(initial_source).unwrap().0.unwrap();
10490
10491 let mut frontend = FrontendState::new();
10492
10493 let mock_ctx = ExecutorContext::new_mock(None).await;
10494 let version = Version(0);
10495
10496 frontend.program = program.clone();
10497 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10498 frontend.update_state_after_exec(outcome, true);
10499 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10500 let sketch_id = sketch_object.id;
10501 let sketch = expect_sketch(sketch_object);
10502 let segments = sketch
10503 .segments
10504 .iter()
10505 .take(3)
10506 .copied()
10507 .map(Into::into)
10508 .collect::<Vec<ConstraintSegment>>();
10509
10510 let constraint = Constraint::Coincident(Coincident {
10511 segments: segments.clone(),
10512 });
10513 let (src_delta, scene_delta) = frontend
10514 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10515 .await
10516 .unwrap();
10517 insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10518
10519 let constraint_object = scene_delta
10520 .new_graph
10521 .objects
10522 .iter()
10523 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10524 .unwrap();
10525
10526 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10527 panic!("expected a constraint object");
10528 };
10529
10530 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10531
10532 mock_ctx.close().await;
10533 }
10534
10535 #[tokio::test(flavor = "multi_thread")]
10536 async fn test_source_with_three_point_coincident_tracks_all_segments() {
10537 let initial_source = "\
10538sketch(on = XY) {
10539 point1 = point(at = [var 1, var 2])
10540 point2 = point(at = [var 3, var 4])
10541 point3 = point(at = [var 5, var 6])
10542 coincident([point1, point2, point3])
10543}
10544";
10545
10546 let program = Program::parse(initial_source).unwrap().0.unwrap();
10547
10548 let mut frontend = FrontendState::new();
10549
10550 let ctx = ExecutorContext::new_mock(None).await;
10551 frontend.program = program.clone();
10552 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10553 frontend.update_state_after_exec(outcome, true);
10554
10555 let constraint_object = frontend
10556 .scene_graph
10557 .objects
10558 .iter()
10559 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10560 .unwrap();
10561 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10562 panic!("expected a constraint object");
10563 };
10564
10565 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10566 let sketch = expect_sketch(sketch_object);
10567 let expected_segments = sketch
10568 .segments
10569 .iter()
10570 .take(3)
10571 .copied()
10572 .map(Into::into)
10573 .collect::<Vec<ConstraintSegment>>();
10574
10575 assert_eq!(
10576 constraint,
10577 &Constraint::Coincident(Coincident {
10578 segments: expected_segments,
10579 })
10580 );
10581
10582 ctx.close().await;
10583 }
10584
10585 #[tokio::test(flavor = "multi_thread")]
10586 async fn test_point_origin_coincident_preserves_order() {
10587 let initial_source = "\
10588sketch(on = XY) {
10589 point(at = [var 1, var 2])
10590}
10591";
10592
10593 for (origin_first, snapshot_name) in [
10594 (true, "test_point_origin_coincident_preserves_order_origin_first"),
10595 (false, "test_point_origin_coincident_preserves_order_point_first"),
10596 ] {
10597 let program = Program::parse(initial_source).unwrap().0.unwrap();
10598
10599 let mut frontend = FrontendState::new();
10600
10601 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10602 let mock_ctx = ExecutorContext::new_mock(None).await;
10603 let version = Version(0);
10604
10605 frontend.hack_set_program(&ctx, program).await.unwrap();
10606 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10607 let sketch_id = sketch_object.id;
10608 let sketch = expect_sketch(sketch_object);
10609 let point_id = *sketch.segments.first().unwrap();
10610
10611 let segments = if origin_first {
10612 vec![ConstraintSegment::ORIGIN, point_id.into()]
10613 } else {
10614 vec![point_id.into(), ConstraintSegment::ORIGIN]
10615 };
10616 let constraint = Constraint::Coincident(Coincident {
10617 segments: segments.clone(),
10618 });
10619 let (src_delta, scene_delta) = frontend
10620 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10621 .await
10622 .unwrap();
10623 insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10624
10625 let constraint_object = scene_delta
10626 .new_graph
10627 .objects
10628 .iter()
10629 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10630 .unwrap();
10631
10632 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10633 panic!("expected a constraint object");
10634 };
10635
10636 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10637
10638 ctx.close().await;
10639 mock_ctx.close().await;
10640 }
10641 }
10642
10643 #[tokio::test(flavor = "multi_thread")]
10644 async fn test_coincident_of_line_end_points() {
10645 let initial_source = "\
10646sketch(on = XY) {
10647 line(start = [var 1, var 2], end = [var 3, var 4])
10648 line(start = [var 5, var 6], end = [var 7, var 8])
10649}
10650";
10651
10652 let program = Program::parse(initial_source).unwrap().0.unwrap();
10653
10654 let mut frontend = FrontendState::new();
10655
10656 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10657 let mock_ctx = ExecutorContext::new_mock(None).await;
10658 let version = Version(0);
10659
10660 frontend.hack_set_program(&ctx, program).await.unwrap();
10661 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10662 let sketch_id = sketch_object.id;
10663 let sketch = expect_sketch(sketch_object);
10664 let point0_id = *sketch.segments.get(1).unwrap();
10665 let point1_id = *sketch.segments.get(3).unwrap();
10666
10667 let constraint = Constraint::Coincident(Coincident {
10668 segments: vec![point0_id.into(), point1_id.into()],
10669 });
10670 let (src_delta, scene_delta) = frontend
10671 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10672 .await
10673 .unwrap();
10674 insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10675 assert_eq!(
10676 scene_delta.new_graph.objects.len(),
10677 9,
10678 "{:#?}",
10679 scene_delta.new_graph.objects
10680 );
10681
10682 ctx.close().await;
10683 mock_ctx.close().await;
10684 }
10685
10686 #[tokio::test(flavor = "multi_thread")]
10687 async fn test_coincident_of_line_point_and_circle_segment() {
10688 let initial_source = "\
10689sketch(on = XY) {
10690 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10691 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10692}
10693";
10694 let program = Program::parse(initial_source).unwrap().0.unwrap();
10695 let mut frontend = FrontendState::new();
10696
10697 let mock_ctx = ExecutorContext::new_mock(None).await;
10698 let version = Version(0);
10699
10700 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10701 frontend.program = program;
10702 frontend.update_state_after_exec(outcome, true);
10703 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10704 let sketch_id = sketch_object.id;
10705 let sketch = expect_sketch(sketch_object);
10706
10707 let circle_id = sketch
10708 .segments
10709 .iter()
10710 .copied()
10711 .find(|seg_id| {
10712 matches!(
10713 &frontend.scene_graph.objects[seg_id.0].kind,
10714 ObjectKind::Segment {
10715 segment: Segment::Circle(_)
10716 }
10717 )
10718 })
10719 .expect("Expected a circle segment in sketch");
10720 let line_id = frontend
10721 .scene_graph
10722 .objects
10723 .iter()
10724 .find_map(|obj| match &obj.kind {
10725 ObjectKind::Segment {
10726 segment: Segment::Line(line),
10727 } if line.owner.is_none() => Some(obj.id),
10728 _ => None,
10729 })
10730 .expect("Expected a standalone line segment in scene graph");
10731
10732 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10733 ObjectKind::Segment {
10734 segment: Segment::Line(line),
10735 } => line.start,
10736 _ => panic!("Expected line segment object"),
10737 };
10738
10739 let constraint = Constraint::Coincident(Coincident {
10740 segments: vec![line_start_point_id.into(), circle_id.into()],
10741 });
10742 let (src_delta, _scene_delta) = frontend
10743 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10744 .await
10745 .unwrap();
10746 insta::assert_snapshot!(
10747 "test_coincident_of_line_point_and_circle_segment",
10748 src_delta.text.as_str()
10749 );
10750
10751 mock_ctx.close().await;
10752 }
10753
10754 #[tokio::test(flavor = "multi_thread")]
10755 async fn test_invalid_coincident_arc_and_line_preserves_state() {
10756 let program = Program::empty();
10764
10765 let mut frontend = FrontendState::new();
10766 frontend.program = program;
10767
10768 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10769 let mock_ctx = ExecutorContext::new_mock(None).await;
10770 let version = Version(0);
10771
10772 let sketch_args = SketchCtor {
10773 on: Plane::Default(PlaneName::Xy),
10774 };
10775 let (_src_delta, _scene_delta, sketch_id) = frontend
10776 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10777 .await
10778 .unwrap();
10779
10780 let arc_ctor = ArcCtor {
10782 start: Point2d {
10783 x: Expr::Var(Number {
10784 value: 0.0,
10785 units: NumericSuffix::Mm,
10786 }),
10787 y: Expr::Var(Number {
10788 value: 0.0,
10789 units: NumericSuffix::Mm,
10790 }),
10791 },
10792 end: Point2d {
10793 x: Expr::Var(Number {
10794 value: 10.0,
10795 units: NumericSuffix::Mm,
10796 }),
10797 y: Expr::Var(Number {
10798 value: 10.0,
10799 units: NumericSuffix::Mm,
10800 }),
10801 },
10802 center: Point2d {
10803 x: Expr::Var(Number {
10804 value: 10.0,
10805 units: NumericSuffix::Mm,
10806 }),
10807 y: Expr::Var(Number {
10808 value: 0.0,
10809 units: NumericSuffix::Mm,
10810 }),
10811 },
10812 direction: None,
10813 construction: None,
10814 };
10815 let (_src_delta, scene_delta) = frontend
10816 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10817 .await
10818 .unwrap();
10819 let arc_id = *scene_delta.new_objects.last().unwrap();
10821
10822 let line_ctor = LineCtor {
10824 start: Point2d {
10825 x: Expr::Var(Number {
10826 value: 20.0,
10827 units: NumericSuffix::Mm,
10828 }),
10829 y: Expr::Var(Number {
10830 value: 0.0,
10831 units: NumericSuffix::Mm,
10832 }),
10833 },
10834 end: Point2d {
10835 x: Expr::Var(Number {
10836 value: 30.0,
10837 units: NumericSuffix::Mm,
10838 }),
10839 y: Expr::Var(Number {
10840 value: 10.0,
10841 units: NumericSuffix::Mm,
10842 }),
10843 },
10844 construction: None,
10845 };
10846 let (_src_delta, scene_delta) = frontend
10847 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10848 .await
10849 .unwrap();
10850 let line_id = *scene_delta.new_objects.last().unwrap();
10852
10853 let constraint = Constraint::Coincident(Coincident {
10856 segments: vec![arc_id.into(), line_id.into()],
10857 });
10858 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10859
10860 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10862
10863 let sketch_object_after =
10866 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10867 let sketch_after = expect_sketch(sketch_object_after);
10868
10869 assert!(
10871 sketch_after.segments.contains(&arc_id),
10872 "Arc segment should still exist after failed constraint"
10873 );
10874 assert!(
10875 sketch_after.segments.contains(&line_id),
10876 "Line segment should still exist after failed constraint"
10877 );
10878
10879 let arc_obj = frontend
10881 .scene_graph
10882 .objects
10883 .get(arc_id.0)
10884 .expect("Arc object should still be accessible");
10885 let line_obj = frontend
10886 .scene_graph
10887 .objects
10888 .get(line_id.0)
10889 .expect("Line object should still be accessible");
10890
10891 match &arc_obj.kind {
10894 ObjectKind::Segment {
10895 segment: Segment::Arc(_),
10896 } => {}
10897 _ => panic!("Arc object should still be an arc segment"),
10898 }
10899 match &line_obj.kind {
10900 ObjectKind::Segment {
10901 segment: Segment::Line(_),
10902 } => {}
10903 _ => panic!("Line object should still be a line segment"),
10904 }
10905
10906 ctx.close().await;
10907 mock_ctx.close().await;
10908 }
10909
10910 #[tokio::test(flavor = "multi_thread")]
10911 async fn test_distance_two_points() {
10912 let initial_source = "\
10913sketch(on = XY) {
10914 point(at = [var 1, var 2])
10915 point(at = [var 3, var 4])
10916}
10917";
10918
10919 let program = Program::parse(initial_source).unwrap().0.unwrap();
10920
10921 let mut frontend = FrontendState::new();
10922
10923 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10924 let mock_ctx = ExecutorContext::new_mock(None).await;
10925 let version = Version(0);
10926
10927 frontend.hack_set_program(&ctx, program).await.unwrap();
10928 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10929 let sketch_id = sketch_object.id;
10930 let sketch = expect_sketch(sketch_object);
10931 let point0_id = *sketch.segments.first().unwrap();
10932 let point1_id = *sketch.segments.get(1).unwrap();
10933
10934 let constraint = Constraint::Distance(Distance {
10935 segments: vec![point0_id.into(), point1_id.into()],
10936 distance: Number {
10937 value: 2.0,
10938 units: NumericSuffix::Mm,
10939 },
10940 label_position: None,
10941 source: Default::default(),
10942 });
10943 let (src_delta, scene_delta) = frontend
10944 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10945 .await
10946 .unwrap();
10947 insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
10948 assert_eq!(
10949 scene_delta.new_graph.objects.len(),
10950 5,
10951 "{:#?}",
10952 scene_delta.new_graph.objects
10953 );
10954
10955 ctx.close().await;
10956 mock_ctx.close().await;
10957 }
10958
10959 #[tokio::test(flavor = "multi_thread")]
10960 async fn test_distance_two_points_with_label() {
10961 let initial_source = "\
10962sketch(on = XY) {
10963 point(at = [var 1, var 2])
10964 point(at = [var 3, var 4])
10965}
10966";
10967
10968 let program = Program::parse(initial_source).unwrap().0.unwrap();
10969
10970 let mut frontend = FrontendState::new();
10971
10972 let mock_ctx = ExecutorContext::new_mock(None).await;
10973 let version = Version(0);
10974
10975 frontend.program = program.clone();
10976 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10977 frontend.update_state_after_exec(outcome, true);
10978 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10979 let sketch_id = sketch_object.id;
10980 let sketch = expect_sketch(sketch_object);
10981 let point0_id = *sketch.segments.first().unwrap();
10982 let point1_id = *sketch.segments.get(1).unwrap();
10983
10984 let label_position = Point2d {
10985 x: Number {
10986 value: 10.0,
10987 units: NumericSuffix::Mm,
10988 },
10989 y: Number {
10990 value: 11.0,
10991 units: NumericSuffix::Mm,
10992 },
10993 };
10994 let constraint = Constraint::Distance(Distance {
10995 segments: vec![point0_id.into(), point1_id.into()],
10996 distance: Number {
10997 value: 2.0,
10998 units: NumericSuffix::Mm,
10999 },
11000 label_position: Some(label_position.clone()),
11001 source: Default::default(),
11002 });
11003 let (src_delta, scene_delta) = frontend
11004 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11005 .await
11006 .unwrap();
11007 insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11008
11009 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11010 let sketch = expect_sketch(sketch_object);
11011 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11012 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11013 panic!("Expected constraint object");
11014 };
11015 let Constraint::Distance(distance) = constraint else {
11016 panic!("Expected distance constraint");
11017 };
11018 assert_eq!(distance.label_position, Some(label_position));
11019
11020 mock_ctx.close().await;
11021 }
11022
11023 #[tokio::test(flavor = "multi_thread")]
11024 async fn test_edit_distance_constraint_label_position() {
11025 let initial_source = "\
11026sketch(on = XY) {
11027 point(at = [var 1, var 2])
11028 point(at = [var 3, var 2])
11029}
11030";
11031
11032 let program = Program::parse(initial_source).unwrap().0.unwrap();
11033
11034 let mut frontend = FrontendState::new();
11035
11036 let mock_ctx = ExecutorContext::new_mock(None).await;
11037 let version = Version(0);
11038
11039 frontend.program = program.clone();
11040 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11041 frontend.update_state_after_exec(outcome, true);
11042 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11043 let sketch_id = sketch_object.id;
11044 let sketch = expect_sketch(sketch_object);
11045 let point0_id = *sketch.segments.first().unwrap();
11046 let point1_id = *sketch.segments.get(1).unwrap();
11047
11048 let constraint = Constraint::Distance(Distance {
11049 segments: vec![point0_id.into(), point1_id.into()],
11050 distance: Number {
11051 value: 2.0,
11052 units: NumericSuffix::Mm,
11053 },
11054 label_position: None,
11055 source: Default::default(),
11056 });
11057 let (_, scene_delta) = frontend
11058 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11059 .await
11060 .unwrap();
11061 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11062 let sketch = expect_sketch(sketch_object);
11063 let constraint_id = sketch.constraints[0];
11064 let label_position = Point2d {
11065 x: Number {
11066 value: 10.0,
11067 units: NumericSuffix::Mm,
11068 },
11069 y: Number {
11070 value: 11.0,
11071 units: NumericSuffix::Mm,
11072 },
11073 };
11074
11075 let (src_delta, scene_delta) = frontend
11076 .edit_distance_constraint_label_position(
11077 &mock_ctx,
11078 version,
11079 sketch_id,
11080 constraint_id,
11081 label_position.clone(),
11082 vec![],
11083 )
11084 .await
11085 .unwrap();
11086 insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11087
11088 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11089 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11090 panic!("Expected constraint object");
11091 };
11092 let Constraint::Distance(distance) = constraint else {
11093 panic!("Expected distance constraint");
11094 };
11095 assert_eq!(distance.label_position, Some(label_position));
11096
11097 mock_ctx.close().await;
11098 }
11099
11100 #[tokio::test(flavor = "multi_thread")]
11101 async fn test_edit_distance_constraint_type_and_value() {
11102 let initial_source = "\
11103sketch(on = XY) {
11104 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11105 distance([line1.start, line1.end]) == 5mm
11106}
11107";
11108
11109 let program = Program::parse(initial_source).unwrap().0.unwrap();
11110 let mut frontend = FrontendState::new();
11111 let mock_ctx = ExecutorContext::new_mock(None).await;
11112 let version = Version(0);
11113
11114 frontend.program = program.clone();
11115 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11116 frontend.update_state_after_exec(outcome, true);
11117 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11118 let sketch_id = sketch_object.id;
11119 let sketch = expect_sketch(sketch_object);
11120 let constraint_id = sketch.constraints[0];
11121 let point0_id = sketch.segments[0];
11122 let point1_id = sketch.segments[1];
11123 let label_position = Point2d {
11124 x: Number {
11125 value: 2.0,
11126 units: NumericSuffix::Mm,
11127 },
11128 y: Number {
11129 value: 5.0,
11130 units: NumericSuffix::Mm,
11131 },
11132 };
11133
11134 let (source_delta, scene_delta) = frontend
11135 .edit_distance_constraint_with_options(
11136 &mock_ctx,
11137 version,
11138 sketch_id,
11139 constraint_id,
11140 Constraint::HorizontalDistance(Distance {
11141 segments: vec![point0_id.into(), point1_id.into()],
11142 distance: Number {
11143 value: 4.0,
11144 units: NumericSuffix::Mm,
11145 },
11146 label_position: Some(label_position.clone()),
11147 source: Default::default(),
11148 }),
11149 EditConstraintOptions {
11150 commit_solved_initial_guesses: false,
11151 },
11152 )
11153 .await
11154 .unwrap();
11155 assert_eq!(
11156 source_delta.text,
11157 "\
11158sketch(on = XY) {
11159 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11160 horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11161}
11162"
11163 );
11164
11165 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11166 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11167 panic!("Expected constraint object");
11168 };
11169 let Constraint::HorizontalDistance(distance) = constraint else {
11170 panic!("Expected horizontal distance constraint");
11171 };
11172 assert_eq!(distance.distance.value, 4.0);
11173 assert_eq!(distance.label_position, Some(label_position));
11174
11175 mock_ctx.close().await;
11176 }
11177
11178 #[tokio::test(flavor = "multi_thread")]
11179 async fn test_edit_angle_constraint_label_position() {
11180 let initial_source = "\
11181sketch(on = XY) {
11182 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11183 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11184 angle([line1, line2]) == 60deg
11185}
11186";
11187
11188 let program = Program::parse(initial_source).unwrap().0.unwrap();
11189 let mut frontend = FrontendState::new();
11190 let mock_ctx = ExecutorContext::new_mock(None).await;
11191 let version = Version(0);
11192
11193 frontend.program = program.clone();
11194 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11195 frontend.update_state_after_exec(outcome, true);
11196 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11197 let sketch_id = sketch_object.id;
11198 let sketch = expect_sketch(sketch_object);
11199 let constraint_id = sketch.constraints[0];
11200 let label_position = Point2d {
11201 x: Number {
11202 value: 10.0,
11203 units: NumericSuffix::Mm,
11204 },
11205 y: Number {
11206 value: 11.0,
11207 units: NumericSuffix::Mm,
11208 },
11209 };
11210
11211 let (src_delta, scene_delta) = frontend
11212 .edit_distance_constraint_label_position(
11213 &mock_ctx,
11214 version,
11215 sketch_id,
11216 constraint_id,
11217 label_position.clone(),
11218 vec![],
11219 )
11220 .await
11221 .unwrap();
11222 assert_eq!(
11223 src_delta.text.as_str(),
11224 "\
11225sketch(on = XY) {
11226 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11227 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11228 angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11229}
11230"
11231 );
11232
11233 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11234 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11235 panic!("Expected constraint object");
11236 };
11237 let Constraint::Angle(angle) = constraint else {
11238 panic!("Expected angle constraint");
11239 };
11240 assert_eq!(angle.label_position, Some(label_position));
11241
11242 mock_ctx.close().await;
11243 }
11244
11245 #[tokio::test(flavor = "multi_thread")]
11246 async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11247 let initial_source = "\
11248sketch(on = XY) {
11249 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11250 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11251 60deg == angleDimension(lines = [line1, line2], sector = 1)
11252}
11253";
11254
11255 let program = Program::parse(initial_source).unwrap().0.unwrap();
11256 let mut frontend = FrontendState::new();
11257 let mock_ctx = ExecutorContext::new_mock(None).await;
11258 let version = Version(0);
11259
11260 frontend.program = program.clone();
11261 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11262 frontend.update_state_after_exec(outcome, true);
11263 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11264 let sketch_id = sketch_object.id;
11265 let sketch = expect_sketch(sketch_object);
11266 let constraint_id = sketch.constraints[0];
11267 let label_position = Point2d {
11268 x: Number {
11269 value: 10.0,
11270 units: NumericSuffix::Mm,
11271 },
11272 y: Number {
11273 value: 11.0,
11274 units: NumericSuffix::Mm,
11275 },
11276 };
11277
11278 let (src_delta, scene_delta) = frontend
11279 .edit_distance_constraint_label_position(
11280 &mock_ctx,
11281 version,
11282 sketch_id,
11283 constraint_id,
11284 label_position.clone(),
11285 vec![],
11286 )
11287 .await
11288 .unwrap();
11289 assert_eq!(
11290 src_delta.text.as_str(),
11291 "\
11292sketch(on = XY) {
11293 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11294 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11295 60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11296}
11297"
11298 );
11299
11300 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11301 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11302 panic!("Expected constraint object");
11303 };
11304 let Constraint::Angle(angle) = constraint else {
11305 panic!("Expected angle constraint");
11306 };
11307 assert_eq!(angle.label_position, Some(label_position));
11308
11309 mock_ctx.close().await;
11310 }
11311
11312 #[tokio::test(flavor = "multi_thread")]
11313 async fn test_edit_angle_constraint() {
11314 let initial_source = "\
11315sketch(on = XY) {
11316 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11317 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11318 angle([line1, line2]) == 60deg
11319}
11320";
11321
11322 let program = Program::parse(initial_source).unwrap().0.unwrap();
11323 let mut frontend = FrontendState::new();
11324 let mock_ctx = ExecutorContext::new_mock(None).await;
11325 let version = Version(0);
11326
11327 frontend.program = program.clone();
11328 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11329 frontend.update_state_after_exec(outcome, true);
11330 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11331 let sketch_id = sketch_object.id;
11332 let sketch = expect_sketch(sketch_object);
11333 let constraint_id = sketch.constraints[0];
11334 let line1_id = *sketch.segments.get(2).unwrap();
11335 let line2_id = *sketch.segments.get(5).unwrap();
11336 let label_position = Point2d {
11337 x: Number {
11338 value: 10.0,
11339 units: NumericSuffix::Mm,
11340 },
11341 y: Number {
11342 value: 11.0,
11343 units: NumericSuffix::Mm,
11344 },
11345 };
11346
11347 let (src_delta, scene_delta) = frontend
11348 .edit_angle_constraint_with_options(
11349 &mock_ctx,
11350 version,
11351 sketch_id,
11352 constraint_id,
11353 Angle {
11354 lines: vec![line2_id, line1_id],
11355 angle: Number {
11356 value: 60.0,
11357 units: NumericSuffix::Deg,
11358 },
11359 sector: Some(3),
11360 inverse: Some(false),
11361 label_position: Some(label_position.clone()),
11362 source: Default::default(),
11363 },
11364 EditConstraintOptions {
11365 commit_solved_initial_guesses: false,
11366 },
11367 )
11368 .await
11369 .unwrap();
11370 assert_eq!(
11371 src_delta.text.as_str(),
11372 "\
11373sketch(on = XY) {
11374 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11375 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11376 angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11377}
11378"
11379 );
11380
11381 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11382 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11383 panic!("Expected constraint object");
11384 };
11385 let Constraint::Angle(angle) = constraint else {
11386 panic!("Expected angle constraint");
11387 };
11388 assert_eq!(angle.lines, vec![line2_id, line1_id]);
11389 assert_eq!(angle.sector, Some(3));
11390 assert_eq!(angle.inverse, Some(false));
11391 assert_eq!(angle.label_position, Some(label_position));
11392
11393 mock_ctx.close().await;
11394 }
11395
11396 #[tokio::test(flavor = "multi_thread")]
11397 async fn test_edit_angle_constraint_with_call_on_right() {
11398 let initial_source = "\
11399sketch(on = XY) {
11400 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11401 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11402 60deg == angle([line1, line2])
11403}
11404";
11405
11406 let program = Program::parse(initial_source).unwrap().0.unwrap();
11407 let mut frontend = FrontendState::new();
11408 let mock_ctx = ExecutorContext::new_mock(None).await;
11409 let version = Version(0);
11410
11411 frontend.program = program.clone();
11412 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11413 frontend.update_state_after_exec(outcome, true);
11414 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11415 let sketch_id = sketch_object.id;
11416 let sketch = expect_sketch(sketch_object);
11417 let constraint_id = sketch.constraints[0];
11418 let line1_id = *sketch.segments.get(2).unwrap();
11419 let line2_id = *sketch.segments.get(5).unwrap();
11420
11421 let (src_delta, _) = frontend
11422 .edit_angle_constraint_with_options(
11423 &mock_ctx,
11424 version,
11425 sketch_id,
11426 constraint_id,
11427 Angle {
11428 lines: vec![line2_id, line1_id],
11429 angle: Number {
11430 value: 60.0,
11431 units: NumericSuffix::Deg,
11432 },
11433 sector: Some(3),
11434 inverse: Some(false),
11435 label_position: None,
11436 source: Default::default(),
11437 },
11438 EditConstraintOptions {
11439 commit_solved_initial_guesses: false,
11440 },
11441 )
11442 .await
11443 .unwrap();
11444 assert_eq!(
11445 src_delta.text.as_str(),
11446 "\
11447sketch(on = XY) {
11448 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11449 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11450 60deg == angleDimension(lines = [line2, line1], sector = 3)
11451}
11452"
11453 );
11454
11455 mock_ctx.close().await;
11456 }
11457
11458 #[tokio::test(flavor = "multi_thread")]
11459 async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11460 let initial_source = "\
11461@settings(kclVersion = 2.0)
11462
11463sketch001 = sketch(on = XZ) {
11464 line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11465 line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11466 distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11467 vertical([line1.start, ORIGIN])
11468}
11469";
11470
11471 let program = Program::parse(initial_source).unwrap().0.unwrap();
11472 let mut frontend = FrontendState::new();
11473 let mock_ctx = ExecutorContext::new_mock(None).await;
11474 let version = Version(0);
11475
11476 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11477 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11478 let sketch_id = sketch_object.id;
11479 let sketch = expect_sketch(sketch_object);
11480 let constraint_id = sketch
11481 .constraints
11482 .iter()
11483 .copied()
11484 .find(|constraint_id| {
11485 matches!(
11486 frontend.scene_graph.objects[constraint_id.0].kind,
11487 ObjectKind::Constraint {
11488 constraint: Constraint::Distance(_)
11489 }
11490 )
11491 })
11492 .unwrap();
11493 let line1_id = sketch
11494 .segments
11495 .iter()
11496 .copied()
11497 .find(|segment_id| {
11498 matches!(
11499 frontend.scene_graph.objects[segment_id.0].kind,
11500 ObjectKind::Segment {
11501 segment: Segment::Line(_)
11502 }
11503 )
11504 })
11505 .unwrap();
11506 let label_position = Point2d {
11507 x: Number {
11508 value: 7.0,
11509 units: NumericSuffix::Mm,
11510 },
11511 y: Number {
11512 value: 9.0,
11513 units: NumericSuffix::Mm,
11514 },
11515 };
11516
11517 let (source_delta, scene_delta) = frontend
11518 .edit_segments_with_options(
11519 &mock_ctx,
11520 version,
11521 sketch_id,
11522 vec![ExistingSegmentCtor {
11523 id: line1_id,
11524 ctor: SegmentCtor::Line(LineCtor {
11525 start: point_expr_mm(2.0, 15.55),
11526 end: point_expr_mm(-4.03, 11.51),
11527 construction: None,
11528 }),
11529 }],
11530 EditSegmentsOptions {
11531 anchor_segment_ids: Some(vec![]),
11532 drag_anchors: vec![SegmentDragAnchor {
11533 segment_id: line1_id,
11534 target: label_position.clone(),
11535 }],
11536 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11537 constraint_id,
11538 label_position: label_position.clone(),
11539 }],
11540 commit_solved_initial_guesses: true,
11541 },
11542 )
11543 .await
11544 .unwrap();
11545
11546 assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11547 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11548 let ObjectKind::Constraint {
11549 constraint: Constraint::Distance(distance),
11550 } = &constraint_object.kind
11551 else {
11552 panic!("Expected distance constraint object");
11553 };
11554 assert_eq!(distance.label_position, Some(label_position));
11555
11556 let snapped_label_position = Point2d {
11557 x: Number {
11558 value: 8.0,
11559 units: NumericSuffix::Mm,
11560 },
11561 y: Number {
11562 value: 10.0,
11563 units: NumericSuffix::Mm,
11564 },
11565 };
11566 let (source_delta, scene_delta) = frontend
11567 .edit_segments_with_options(
11568 &mock_ctx,
11569 version,
11570 sketch_id,
11571 vec![],
11572 EditSegmentsOptions {
11573 anchor_segment_ids: Some(vec![line1_id]),
11574 drag_anchors: vec![],
11575 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11576 constraint_id,
11577 label_position: snapped_label_position.clone(),
11578 }],
11579 commit_solved_initial_guesses: true,
11580 },
11581 )
11582 .await
11583 .unwrap();
11584
11585 assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11586 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11587 let ObjectKind::Constraint {
11588 constraint: Constraint::Distance(distance),
11589 } = &constraint_object.kind
11590 else {
11591 panic!("Expected distance constraint object");
11592 };
11593 assert_eq!(distance.label_position, Some(snapped_label_position));
11594
11595 mock_ctx.close().await;
11596 }
11597
11598 #[tokio::test(flavor = "multi_thread")]
11599 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11600 let initial_source = "\
11601sketch(on = XY) {
11602 point1 = point(at = [var 0mm, var 0mm])
11603 point2 = point(at = [var 10mm, var 0mm])
11604 distance([point1, point2]) == 5mm
11605}
11606";
11607
11608 let program = Program::parse(initial_source).unwrap().0.unwrap();
11609 let mut frontend = FrontendState::new();
11610 let mock_ctx = ExecutorContext::new_mock(None).await;
11611 let version = Version(0);
11612
11613 frontend.program = program.clone();
11614 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11615 frontend.update_state_after_exec(outcome, true);
11616 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11617 let sketch_id = sketch_object.id;
11618 let sketch = expect_sketch(sketch_object);
11619 let point0_id = sketch.segments[0];
11620 let point1_id = sketch.segments[1];
11621 let constraint_id = sketch.constraints[0];
11622
11623 let edited_segments = vec![ExistingSegmentCtor {
11624 id: point0_id,
11625 ctor: SegmentCtor::Point(PointCtor {
11626 position: Point2d {
11627 x: Expr::Var(Number {
11628 value: 2.0,
11629 units: NumericSuffix::Mm,
11630 }),
11631 y: Expr::Var(Number {
11632 value: 1.0,
11633 units: NumericSuffix::Mm,
11634 }),
11635 },
11636 }),
11637 }];
11638 let (_, scene_delta) = frontend
11639 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11640 .await
11641 .unwrap();
11642 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11643 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11644
11645 let label_position = Point2d {
11646 x: Number {
11647 value: 3.0,
11648 units: NumericSuffix::Mm,
11649 },
11650 y: Number {
11651 value: 4.0,
11652 units: NumericSuffix::Mm,
11653 },
11654 };
11655 let (_, scene_delta) = frontend
11656 .edit_distance_constraint_label_position(
11657 &mock_ctx,
11658 version,
11659 sketch_id,
11660 constraint_id,
11661 label_position,
11662 vec![point0_id],
11663 )
11664 .await
11665 .unwrap();
11666
11667 assert_point_position_close(
11668 point_position(&scene_delta.new_graph, point0_id),
11669 point0_after_segment_edit,
11670 );
11671 assert_point_position_close(
11672 point_position(&scene_delta.new_graph, point1_id),
11673 point1_after_segment_edit,
11674 );
11675
11676 mock_ctx.close().await;
11677 }
11678
11679 #[tokio::test(flavor = "multi_thread")]
11680 async fn test_distance_point_line() {
11681 let initial_source = "\
11682sketch(on = XY) {
11683 point(at = [var 0, var 5])
11684 line(start = [var 0, var 0], end = [var 10, var 0])
11685}
11686";
11687
11688 let program = Program::parse(initial_source).unwrap().0.unwrap();
11689
11690 let mut frontend = FrontendState::new();
11691
11692 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11693 let mock_ctx = ExecutorContext::new_mock(None).await;
11694 let version = Version(0);
11695
11696 frontend.hack_set_program(&ctx, program).await.unwrap();
11697 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11698 let sketch_id = sketch_object.id;
11699 let sketch = expect_sketch(sketch_object);
11700 let point_id = *sketch.segments.first().unwrap();
11701 let line_id = *sketch
11702 .segments
11703 .iter()
11704 .find(|segment_id| {
11705 matches!(
11706 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11707 Some(ObjectKind::Segment {
11708 segment: Segment::Line(_)
11709 })
11710 )
11711 })
11712 .unwrap();
11713
11714 let label_position = Point2d {
11715 x: Number {
11716 value: 10.0,
11717 units: NumericSuffix::Mm,
11718 },
11719 y: Number {
11720 value: 11.0,
11721 units: NumericSuffix::Mm,
11722 },
11723 };
11724 let constraint = Constraint::Distance(Distance {
11725 segments: vec![point_id.into(), line_id.into()],
11726 distance: Number {
11727 value: 5.0,
11728 units: NumericSuffix::Mm,
11729 },
11730 label_position: Some(label_position.clone()),
11731 source: Default::default(),
11732 });
11733 let (src_delta, scene_delta) = frontend
11734 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11735 .await
11736 .unwrap();
11737 insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11738 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11739 let sketch = expect_sketch(sketch_object);
11740 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11741 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11742 panic!("Expected constraint object");
11743 };
11744 let Constraint::Distance(distance) = constraint else {
11745 panic!("Expected distance constraint");
11746 };
11747 assert_eq!(distance.label_position, Some(label_position));
11748
11749 ctx.close().await;
11750 mock_ctx.close().await;
11751 }
11752
11753 #[tokio::test(flavor = "multi_thread")]
11754 async fn test_distance_point_arc() {
11755 let initial_source = "\
11756sketch(on = XY) {
11757 point(at = [var 0, var 8])
11758 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11759}
11760";
11761
11762 let program = Program::parse(initial_source).unwrap().0.unwrap();
11763
11764 let mut frontend = FrontendState::new();
11765
11766 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11767 let mock_ctx = ExecutorContext::new_mock(None).await;
11768 let version = Version(0);
11769
11770 frontend.hack_set_program(&ctx, program).await.unwrap();
11771 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11772 let sketch_id = sketch_object.id;
11773 let sketch = expect_sketch(sketch_object);
11774 let point_id = *sketch.segments.first().unwrap();
11775 let arc_id = *sketch
11776 .segments
11777 .iter()
11778 .find(|segment_id| {
11779 matches!(
11780 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11781 Some(ObjectKind::Segment {
11782 segment: Segment::Arc(_)
11783 })
11784 )
11785 })
11786 .unwrap();
11787
11788 let constraint = Constraint::Distance(Distance {
11789 segments: vec![point_id.into(), arc_id.into()],
11790 distance: Number {
11791 value: 3.0,
11792 units: NumericSuffix::Mm,
11793 },
11794 label_position: None,
11795 source: Default::default(),
11796 });
11797 let (src_delta, _scene_delta) = frontend
11798 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11799 .await
11800 .unwrap();
11801 insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11802
11803 ctx.close().await;
11804 mock_ctx.close().await;
11805 }
11806
11807 #[tokio::test(flavor = "multi_thread")]
11808 async fn test_distance_arc_origin() {
11809 let initial_source = "\
11810sketch001 = sketch(on = XY) {
11811 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11812}
11813";
11814
11815 let program = Program::parse(initial_source).unwrap().0.unwrap();
11816
11817 let mut frontend = FrontendState::new();
11818
11819 let mock_ctx = ExecutorContext::new_mock(None).await;
11820 let version = Version(0);
11821
11822 frontend.program = program.clone();
11823 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11824 frontend.update_state_after_exec(outcome, true);
11825 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11826 let sketch_id = sketch_object.id;
11827 let sketch = expect_sketch(sketch_object);
11828 let arc_id = *sketch
11829 .segments
11830 .iter()
11831 .find(|segment_id| {
11832 matches!(
11833 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11834 Some(ObjectKind::Segment {
11835 segment: Segment::Arc(_)
11836 })
11837 )
11838 })
11839 .unwrap();
11840
11841 let constraint = Constraint::Distance(Distance {
11842 segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11843 distance: Number {
11844 value: 3.0,
11845 units: NumericSuffix::Mm,
11846 },
11847 label_position: None,
11848 source: Default::default(),
11849 });
11850 let (src_delta, _scene_delta) = frontend
11851 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11852 .await
11853 .unwrap();
11854 insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11855
11856 mock_ctx.close().await;
11857 }
11858
11859 #[tokio::test(flavor = "multi_thread")]
11860 async fn test_distance_line_origin() {
11861 let initial_source = "\
11862sketch(on = XY) {
11863 line(start = [var 5, var 0], end = [var 5, var 10])
11864}
11865";
11866
11867 let program = Program::parse(initial_source).unwrap().0.unwrap();
11868
11869 let mut frontend = FrontendState::new();
11870
11871 let mock_ctx = ExecutorContext::new_mock(None).await;
11872 let version = Version(0);
11873
11874 frontend.program = program.clone();
11875 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11876 frontend.update_state_after_exec(outcome, true);
11877 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11878 let sketch_id = sketch_object.id;
11879 let sketch = expect_sketch(sketch_object);
11880 let line_id = *sketch
11881 .segments
11882 .iter()
11883 .find(|segment_id| {
11884 matches!(
11885 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11886 Some(ObjectKind::Segment {
11887 segment: Segment::Line(_)
11888 })
11889 )
11890 })
11891 .unwrap();
11892
11893 let constraint = Constraint::Distance(Distance {
11894 segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11895 distance: Number {
11896 value: 5.0,
11897 units: NumericSuffix::Mm,
11898 },
11899 label_position: None,
11900 source: Default::default(),
11901 });
11902 let (src_delta, _scene_delta) = frontend
11903 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11904 .await
11905 .unwrap();
11906 insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11907
11908 mock_ctx.close().await;
11909 }
11910
11911 #[tokio::test(flavor = "multi_thread")]
11912 async fn test_distance_line_circle() {
11913 let initial_source = "\
11914sketch(on = XY) {
11915 line(start = [var -10, var 8], end = [var 10, var 8])
11916 circle(start = [var 5, var 0], center = [var 0, var 0])
11917}
11918";
11919
11920 let program = Program::parse(initial_source).unwrap().0.unwrap();
11921
11922 let mut frontend = FrontendState::new();
11923
11924 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11925 let mock_ctx = ExecutorContext::new_mock(None).await;
11926 let version = Version(0);
11927
11928 frontend.hack_set_program(&ctx, program).await.unwrap();
11929 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11930 let sketch_id = sketch_object.id;
11931 let sketch = expect_sketch(sketch_object);
11932 let line_id = *sketch
11933 .segments
11934 .iter()
11935 .find(|segment_id| {
11936 matches!(
11937 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11938 Some(ObjectKind::Segment {
11939 segment: Segment::Line(_)
11940 })
11941 )
11942 })
11943 .unwrap();
11944 let circle_id = *sketch
11945 .segments
11946 .iter()
11947 .find(|segment_id| {
11948 matches!(
11949 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11950 Some(ObjectKind::Segment {
11951 segment: Segment::Circle(_)
11952 })
11953 )
11954 })
11955 .unwrap();
11956
11957 let constraint = Constraint::Distance(Distance {
11958 segments: vec![line_id.into(), circle_id.into()],
11959 distance: Number {
11960 value: 3.0,
11961 units: NumericSuffix::Mm,
11962 },
11963 label_position: None,
11964 source: Default::default(),
11965 });
11966 let (src_delta, _scene_delta) = frontend
11967 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11968 .await
11969 .unwrap();
11970 insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
11971
11972 ctx.close().await;
11973 mock_ctx.close().await;
11974 }
11975
11976 #[tokio::test(flavor = "multi_thread")]
11977 async fn test_distance_circle_arc() {
11978 let initial_source = "\
11979sketch(on = XY) {
11980 circle(start = [var 5, var 0], center = [var 0, var 0])
11981 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11982}
11983";
11984
11985 let program = Program::parse(initial_source).unwrap().0.unwrap();
11986
11987 let mut frontend = FrontendState::new();
11988
11989 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11990 let mock_ctx = ExecutorContext::new_mock(None).await;
11991 let version = Version(0);
11992
11993 frontend.hack_set_program(&ctx, program).await.unwrap();
11994 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11995 let sketch_id = sketch_object.id;
11996 let sketch = expect_sketch(sketch_object);
11997 let circle_id = *sketch
11998 .segments
11999 .iter()
12000 .find(|segment_id| {
12001 matches!(
12002 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12003 Some(ObjectKind::Segment {
12004 segment: Segment::Circle(_)
12005 })
12006 )
12007 })
12008 .unwrap();
12009 let arc_id = *sketch
12010 .segments
12011 .iter()
12012 .find(|segment_id| {
12013 matches!(
12014 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12015 Some(ObjectKind::Segment {
12016 segment: Segment::Arc(_)
12017 })
12018 )
12019 })
12020 .unwrap();
12021
12022 let constraint = Constraint::Distance(Distance {
12023 segments: vec![circle_id.into(), arc_id.into()],
12024 distance: Number {
12025 value: 3.0,
12026 units: NumericSuffix::Mm,
12027 },
12028 label_position: None,
12029 source: Default::default(),
12030 });
12031 let (src_delta, _scene_delta) = frontend
12032 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12033 .await
12034 .unwrap();
12035 insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12036
12037 ctx.close().await;
12038 mock_ctx.close().await;
12039 }
12040
12041 #[tokio::test(flavor = "multi_thread")]
12042 async fn test_distance_parallel_lines() {
12043 let initial_source = "\
12044sketch(on = XY) {
12045 line(start = [var 0, var 0], end = [var 10, var 0])
12046 line(start = [var 0, var 5], end = [var 10, var 5])
12047}
12048";
12049
12050 let program = Program::parse(initial_source).unwrap().0.unwrap();
12051
12052 let mut frontend = FrontendState::new();
12053
12054 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12055 let mock_ctx = ExecutorContext::new_mock(None).await;
12056 let version = Version(0);
12057
12058 frontend.hack_set_program(&ctx, program).await.unwrap();
12059 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12060 let sketch_id = sketch_object.id;
12061 let sketch = expect_sketch(sketch_object);
12062 let line_ids = sketch
12063 .segments
12064 .iter()
12065 .copied()
12066 .filter(|segment_id| {
12067 matches!(
12068 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12069 Some(ObjectKind::Segment {
12070 segment: Segment::Line(_)
12071 })
12072 )
12073 })
12074 .collect::<Vec<_>>();
12075
12076 let constraint = Constraint::Distance(Distance {
12077 segments: vec![line_ids[0].into(), line_ids[1].into()],
12078 distance: Number {
12079 value: 5.0,
12080 units: NumericSuffix::Mm,
12081 },
12082 label_position: None,
12083 source: Default::default(),
12084 });
12085 let (src_delta, _scene_delta) = frontend
12086 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12087 .await
12088 .unwrap();
12089 insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12090
12091 ctx.close().await;
12092 mock_ctx.close().await;
12093 }
12094
12095 #[tokio::test(flavor = "multi_thread")]
12096 async fn test_distance_non_parallel_lines_lowers_to_distance() {
12097 let initial_source = "\
12102sketch(on = XY) {
12103 line(start = [var 0, var 0], end = [var 10, var 0])
12104 line(start = [var 0, var 0], end = [var 10, var 10])
12105}
12106";
12107
12108 let program = Program::parse(initial_source).unwrap().0.unwrap();
12109
12110 let mut frontend = FrontendState::new();
12111
12112 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12113 let mock_ctx = ExecutorContext::new_mock(None).await;
12114 let version = Version(0);
12115
12116 frontend.hack_set_program(&ctx, program).await.unwrap();
12117 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12118 let sketch_id = sketch_object.id;
12119 let sketch = expect_sketch(sketch_object);
12120 let line_ids = sketch
12121 .segments
12122 .iter()
12123 .copied()
12124 .filter(|segment_id| {
12125 matches!(
12126 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12127 Some(ObjectKind::Segment {
12128 segment: Segment::Line(_)
12129 })
12130 )
12131 })
12132 .collect::<Vec<_>>();
12133
12134 let constraint = Constraint::Distance(Distance {
12135 segments: vec![line_ids[0].into(), line_ids[1].into()],
12136 distance: Number {
12137 value: 5.0,
12138 units: NumericSuffix::Mm,
12139 },
12140 label_position: None,
12141 source: Default::default(),
12142 });
12143 let (src_delta, _scene_delta) = frontend
12144 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12145 .await
12146 .unwrap();
12147 insta::assert_snapshot!(
12148 "test_distance_non_parallel_lines_lowers_to_distance",
12149 src_delta.text.as_str()
12150 );
12151
12152 ctx.close().await;
12153 mock_ctx.close().await;
12154 }
12155
12156 #[tokio::test(flavor = "multi_thread")]
12157 async fn test_horizontal_distance_two_points() {
12158 let initial_source = "\
12159sketch(on = XY) {
12160 point(at = [var 1, var 2])
12161 point(at = [var 3, var 4])
12162}
12163";
12164
12165 let program = Program::parse(initial_source).unwrap().0.unwrap();
12166
12167 let mut frontend = FrontendState::new();
12168
12169 let mock_ctx = ExecutorContext::new_mock(None).await;
12170 let version = Version(0);
12171
12172 frontend.program = program.clone();
12173 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12174 frontend.update_state_after_exec(outcome, true);
12175 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12176 let sketch_id = sketch_object.id;
12177 let sketch = expect_sketch(sketch_object);
12178 let point0_id = *sketch.segments.first().unwrap();
12179 let point1_id = *sketch.segments.get(1).unwrap();
12180 let label_position = Point2d {
12181 x: Number {
12182 value: 10.0,
12183 units: NumericSuffix::Mm,
12184 },
12185 y: Number {
12186 value: 11.0,
12187 units: NumericSuffix::Mm,
12188 },
12189 };
12190
12191 let constraint = Constraint::HorizontalDistance(Distance {
12192 segments: vec![point0_id.into(), point1_id.into()],
12193 distance: Number {
12194 value: 2.0,
12195 units: NumericSuffix::Mm,
12196 },
12197 label_position: Some(label_position.clone()),
12198 source: Default::default(),
12199 });
12200 let (src_delta, scene_delta) = frontend
12201 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12202 .await
12203 .unwrap();
12204 insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12205 assert_eq!(
12206 scene_delta.new_graph.objects.len(),
12207 5,
12208 "{:#?}",
12209 scene_delta.new_graph.objects
12210 );
12211 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12212 let sketch = expect_sketch(sketch_object);
12213 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12214 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12215 panic!("Expected constraint object");
12216 };
12217 let Constraint::HorizontalDistance(distance) = constraint else {
12218 panic!("Expected horizontal distance constraint");
12219 };
12220 assert_eq!(distance.label_position, Some(label_position));
12221
12222 mock_ctx.close().await;
12223 }
12224
12225 #[tokio::test(flavor = "multi_thread")]
12226 async fn test_radius_single_arc_segment() {
12227 let initial_source = "\
12228sketch(on = XY) {
12229 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12230}
12231";
12232
12233 let program = Program::parse(initial_source).unwrap().0.unwrap();
12234
12235 let mut frontend = FrontendState::new();
12236
12237 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12238 let mock_ctx = ExecutorContext::new_mock(None).await;
12239 let version = Version(0);
12240
12241 frontend.hack_set_program(&ctx, program).await.unwrap();
12242 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12243 let sketch_id = sketch_object.id;
12244 let sketch = expect_sketch(sketch_object);
12245 let arc_id = sketch
12247 .segments
12248 .iter()
12249 .find(|&seg_id| {
12250 let obj = frontend.scene_graph.objects.get(seg_id.0);
12251 matches!(
12252 obj.map(|o| &o.kind),
12253 Some(ObjectKind::Segment {
12254 segment: Segment::Arc(_)
12255 })
12256 )
12257 })
12258 .unwrap();
12259
12260 let constraint = Constraint::Radius(Radius {
12261 arc: *arc_id,
12262 radius: Number {
12263 value: 5.0,
12264 units: NumericSuffix::Mm,
12265 },
12266 label_position: None,
12267 source: Default::default(),
12268 });
12269 let (src_delta, scene_delta) = frontend
12270 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12271 .await
12272 .unwrap();
12273 insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12274 assert_eq!(
12275 scene_delta.new_graph.objects.len(),
12276 7, "{:#?}",
12278 scene_delta.new_graph.objects
12279 );
12280
12281 ctx.close().await;
12282 mock_ctx.close().await;
12283 }
12284
12285 #[tokio::test(flavor = "multi_thread")]
12286 async fn test_radius_single_arc_segment_with_label_position() {
12287 let initial_source = "\
12288sketch(on = XY) {
12289 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12290}
12291";
12292
12293 let program = Program::parse(initial_source).unwrap().0.unwrap();
12294 let mut frontend = FrontendState::new();
12295 let mock_ctx = ExecutorContext::new_mock(None).await;
12296 let version = Version(0);
12297
12298 frontend.program = program.clone();
12299 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12300 frontend.update_state_after_exec(outcome, true);
12301 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12302 let sketch_id = sketch_object.id;
12303 let sketch = expect_sketch(sketch_object);
12304 let arc_id = sketch
12305 .segments
12306 .iter()
12307 .find(|&seg_id| {
12308 let obj = frontend.scene_graph.objects.get(seg_id.0);
12309 matches!(
12310 obj.map(|o| &o.kind),
12311 Some(ObjectKind::Segment {
12312 segment: Segment::Arc(_)
12313 })
12314 )
12315 })
12316 .unwrap();
12317
12318 let label_position = Point2d {
12319 x: Number {
12320 value: 10.0,
12321 units: NumericSuffix::Mm,
12322 },
12323 y: Number {
12324 value: 11.0,
12325 units: NumericSuffix::Mm,
12326 },
12327 };
12328 let constraint = Constraint::Radius(Radius {
12329 arc: *arc_id,
12330 radius: Number {
12331 value: 5.0,
12332 units: NumericSuffix::Mm,
12333 },
12334 label_position: Some(label_position.clone()),
12335 source: Default::default(),
12336 });
12337 let (src_delta, scene_delta) = frontend
12338 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12339 .await
12340 .unwrap();
12341 insta::assert_snapshot!(
12342 "test_radius_single_arc_segment_with_label_position",
12343 src_delta.text.as_str()
12344 );
12345
12346 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12347 let sketch = expect_sketch(sketch_object);
12348 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12349 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12350 panic!("Expected constraint object");
12351 };
12352 let Constraint::Radius(radius) = constraint else {
12353 panic!("Expected radius constraint");
12354 };
12355 assert_eq!(radius.label_position, Some(label_position));
12356
12357 mock_ctx.close().await;
12358 }
12359
12360 #[tokio::test(flavor = "multi_thread")]
12361 async fn test_edit_radius_constraint_label_position() {
12362 let initial_source = "\
12363sketch(on = XY) {
12364 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12365 radius(arc1) == 5mm
12366}
12367";
12368
12369 let program = Program::parse(initial_source).unwrap().0.unwrap();
12370 let mut frontend = FrontendState::new();
12371 let mock_ctx = ExecutorContext::new_mock(None).await;
12372 let version = Version(0);
12373
12374 frontend.program = program.clone();
12375 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12376 frontend.update_state_after_exec(outcome, true);
12377 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12378 let sketch_id = sketch_object.id;
12379 let sketch = expect_sketch(sketch_object);
12380 let constraint_id = sketch.constraints[0];
12381 let label_position = Point2d {
12382 x: Number {
12383 value: 10.0,
12384 units: NumericSuffix::Mm,
12385 },
12386 y: Number {
12387 value: 11.0,
12388 units: NumericSuffix::Mm,
12389 },
12390 };
12391
12392 let (src_delta, scene_delta) = frontend
12393 .edit_distance_constraint_label_position(
12394 &mock_ctx,
12395 version,
12396 sketch_id,
12397 constraint_id,
12398 label_position.clone(),
12399 vec![],
12400 )
12401 .await
12402 .unwrap();
12403 insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12404
12405 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12406 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12407 panic!("Expected constraint object");
12408 };
12409 let Constraint::Radius(radius) = constraint else {
12410 panic!("Expected radius constraint");
12411 };
12412 assert_eq!(radius.label_position, Some(label_position));
12413
12414 mock_ctx.close().await;
12415 }
12416
12417 #[tokio::test(flavor = "multi_thread")]
12418 async fn test_vertical_distance_two_points() {
12419 let initial_source = "\
12420sketch(on = XY) {
12421 point(at = [var 1, var 2])
12422 point(at = [var 3, var 4])
12423}
12424";
12425
12426 let program = Program::parse(initial_source).unwrap().0.unwrap();
12427
12428 let mut frontend = FrontendState::new();
12429
12430 let mock_ctx = ExecutorContext::new_mock(None).await;
12431 let version = Version(0);
12432
12433 frontend.program = program.clone();
12434 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12435 frontend.update_state_after_exec(outcome, true);
12436 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12437 let sketch_id = sketch_object.id;
12438 let sketch = expect_sketch(sketch_object);
12439 let point0_id = *sketch.segments.first().unwrap();
12440 let point1_id = *sketch.segments.get(1).unwrap();
12441 let label_position = Point2d {
12442 x: Number {
12443 value: 10.0,
12444 units: NumericSuffix::Mm,
12445 },
12446 y: Number {
12447 value: 11.0,
12448 units: NumericSuffix::Mm,
12449 },
12450 };
12451
12452 let constraint = Constraint::VerticalDistance(Distance {
12453 segments: vec![point0_id.into(), point1_id.into()],
12454 distance: Number {
12455 value: 2.0,
12456 units: NumericSuffix::Mm,
12457 },
12458 label_position: Some(label_position.clone()),
12459 source: Default::default(),
12460 });
12461 let (src_delta, scene_delta) = frontend
12462 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12463 .await
12464 .unwrap();
12465 insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12466 assert_eq!(
12467 scene_delta.new_graph.objects.len(),
12468 5,
12469 "{:#?}",
12470 scene_delta.new_graph.objects
12471 );
12472 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12473 let sketch = expect_sketch(sketch_object);
12474 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12475 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12476 panic!("Expected constraint object");
12477 };
12478 let Constraint::VerticalDistance(distance) = constraint else {
12479 panic!("Expected vertical distance constraint");
12480 };
12481 assert_eq!(distance.label_position, Some(label_position));
12482
12483 mock_ctx.close().await;
12484 }
12485
12486 #[tokio::test(flavor = "multi_thread")]
12487 async fn test_add_fixed_standalone_point() {
12488 let initial_source = "\
12489sketch(on = XY) {
12490 point(at = [var 1, var 2])
12491}
12492";
12493
12494 let program = Program::parse(initial_source).unwrap().0.unwrap();
12495
12496 let mut frontend = FrontendState::new();
12497
12498 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12499 let mock_ctx = ExecutorContext::new_mock(None).await;
12500 let version = Version(0);
12501
12502 frontend.hack_set_program(&ctx, program).await.unwrap();
12503 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12504 let sketch_id = sketch_object.id;
12505 let sketch = expect_sketch(sketch_object);
12506 let point_id = *sketch.segments.first().unwrap();
12507
12508 let (src_delta, scene_delta) = frontend
12509 .add_constraint(
12510 &mock_ctx,
12511 version,
12512 sketch_id,
12513 Constraint::Fixed(Fixed {
12514 points: vec![FixedPoint {
12515 point: point_id,
12516 position: Point2d {
12517 x: Number {
12518 value: 2.0,
12519 units: NumericSuffix::Mm,
12520 },
12521 y: Number {
12522 value: 3.0,
12523 units: NumericSuffix::Mm,
12524 },
12525 },
12526 }],
12527 }),
12528 )
12529 .await
12530 .unwrap();
12531 insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12532 assert_eq!(
12533 scene_delta.new_graph.objects.len(),
12534 4,
12535 "{:#?}",
12536 scene_delta.new_graph.objects
12537 );
12538
12539 ctx.close().await;
12540 mock_ctx.close().await;
12541 }
12542
12543 #[tokio::test(flavor = "multi_thread")]
12544 async fn test_add_fixed_multiple_points() {
12545 let initial_source = "\
12546sketch(on = XY) {
12547 point(at = [var 1, var 2])
12548 point(at = [var 3, var 4])
12549}
12550";
12551
12552 let program = Program::parse(initial_source).unwrap().0.unwrap();
12553
12554 let mut frontend = FrontendState::new();
12555
12556 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12557 let mock_ctx = ExecutorContext::new_mock(None).await;
12558 let version = Version(0);
12559
12560 frontend.hack_set_program(&ctx, program).await.unwrap();
12561 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12562 let sketch_id = sketch_object.id;
12563 let sketch = expect_sketch(sketch_object);
12564 let point0_id = *sketch.segments.first().unwrap();
12565 let point1_id = *sketch.segments.get(1).unwrap();
12566
12567 let (src_delta, scene_delta) = frontend
12568 .add_constraint(
12569 &mock_ctx,
12570 version,
12571 sketch_id,
12572 Constraint::Fixed(Fixed {
12573 points: vec![
12574 FixedPoint {
12575 point: point0_id,
12576 position: Point2d {
12577 x: Number {
12578 value: 2.0,
12579 units: NumericSuffix::Mm,
12580 },
12581 y: Number {
12582 value: 3.0,
12583 units: NumericSuffix::Mm,
12584 },
12585 },
12586 },
12587 FixedPoint {
12588 point: point1_id,
12589 position: Point2d {
12590 x: Number {
12591 value: 4.0,
12592 units: NumericSuffix::Mm,
12593 },
12594 y: Number {
12595 value: 5.0,
12596 units: NumericSuffix::Mm,
12597 },
12598 },
12599 },
12600 ],
12601 }),
12602 )
12603 .await
12604 .unwrap();
12605 insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12606 assert_eq!(
12607 scene_delta.new_graph.objects.len(),
12608 6,
12609 "{:#?}",
12610 scene_delta.new_graph.objects
12611 );
12612
12613 ctx.close().await;
12614 mock_ctx.close().await;
12615 }
12616
12617 #[tokio::test(flavor = "multi_thread")]
12618 async fn test_add_fixed_owned_point() {
12619 let initial_source = "\
12620sketch(on = XY) {
12621 line(start = [var 1, var 2], end = [var 3, var 4])
12622}
12623";
12624
12625 let program = Program::parse(initial_source).unwrap().0.unwrap();
12626
12627 let mut frontend = FrontendState::new();
12628
12629 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12630 let mock_ctx = ExecutorContext::new_mock(None).await;
12631 let version = Version(0);
12632
12633 frontend.hack_set_program(&ctx, program).await.unwrap();
12634 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12635 let sketch_id = sketch_object.id;
12636 let sketch = expect_sketch(sketch_object);
12637 let line_start_id = *sketch.segments.first().unwrap();
12638
12639 let (src_delta, scene_delta) = frontend
12640 .add_constraint(
12641 &mock_ctx,
12642 version,
12643 sketch_id,
12644 Constraint::Fixed(Fixed {
12645 points: vec![FixedPoint {
12646 point: line_start_id,
12647 position: Point2d {
12648 x: Number {
12649 value: 2.0,
12650 units: NumericSuffix::Mm,
12651 },
12652 y: Number {
12653 value: 3.0,
12654 units: NumericSuffix::Mm,
12655 },
12656 },
12657 }],
12658 }),
12659 )
12660 .await
12661 .unwrap();
12662 insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12663 assert_eq!(
12664 scene_delta.new_graph.objects.len(),
12665 6,
12666 "{:#?}",
12667 scene_delta.new_graph.objects
12668 );
12669
12670 ctx.close().await;
12671 mock_ctx.close().await;
12672 }
12673
12674 #[tokio::test(flavor = "multi_thread")]
12675 async fn test_radius_error_cases() {
12676 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12677 let mock_ctx = ExecutorContext::new_mock(None).await;
12678 let version = Version(0);
12679
12680 let initial_source_point = "\
12682sketch(on = XY) {
12683 point(at = [var 1, var 2])
12684}
12685";
12686 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12687 let mut frontend_point = FrontendState::new();
12688 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12689 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12690 let sketch_id_point = sketch_object_point.id;
12691 let sketch_point = expect_sketch(sketch_object_point);
12692 let point_id = *sketch_point.segments.first().unwrap();
12693
12694 let constraint_point = Constraint::Radius(Radius {
12695 arc: point_id,
12696 radius: Number {
12697 value: 5.0,
12698 units: NumericSuffix::Mm,
12699 },
12700 label_position: None,
12701 source: Default::default(),
12702 });
12703 let result_point = frontend_point
12704 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12705 .await;
12706 assert!(result_point.is_err(), "Single point should error for radius");
12707
12708 let initial_source_line = "\
12710sketch(on = XY) {
12711 line(start = [var 1, var 2], end = [var 3, var 4])
12712}
12713";
12714 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12715 let mut frontend_line = FrontendState::new();
12716 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12717 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12718 let sketch_id_line = sketch_object_line.id;
12719 let sketch_line = expect_sketch(sketch_object_line);
12720 let line_id = *sketch_line.segments.first().unwrap();
12721
12722 let constraint_line = Constraint::Radius(Radius {
12723 arc: line_id,
12724 radius: Number {
12725 value: 5.0,
12726 units: NumericSuffix::Mm,
12727 },
12728 label_position: None,
12729 source: Default::default(),
12730 });
12731 let result_line = frontend_line
12732 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12733 .await;
12734 assert!(result_line.is_err(), "Single line segment should error for radius");
12735
12736 ctx.close().await;
12737 mock_ctx.close().await;
12738 }
12739
12740 #[tokio::test(flavor = "multi_thread")]
12741 async fn test_diameter_single_arc_segment() {
12742 let initial_source = "\
12743sketch(on = XY) {
12744 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12745}
12746";
12747
12748 let program = Program::parse(initial_source).unwrap().0.unwrap();
12749
12750 let mut frontend = FrontendState::new();
12751
12752 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12753 let mock_ctx = ExecutorContext::new_mock(None).await;
12754 let version = Version(0);
12755
12756 frontend.hack_set_program(&ctx, program).await.unwrap();
12757 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12758 let sketch_id = sketch_object.id;
12759 let sketch = expect_sketch(sketch_object);
12760 let arc_id = sketch
12762 .segments
12763 .iter()
12764 .find(|&seg_id| {
12765 let obj = frontend.scene_graph.objects.get(seg_id.0);
12766 matches!(
12767 obj.map(|o| &o.kind),
12768 Some(ObjectKind::Segment {
12769 segment: Segment::Arc(_)
12770 })
12771 )
12772 })
12773 .unwrap();
12774
12775 let constraint = Constraint::Diameter(Diameter {
12776 arc: *arc_id,
12777 diameter: Number {
12778 value: 10.0,
12779 units: NumericSuffix::Mm,
12780 },
12781 label_position: None,
12782 source: Default::default(),
12783 });
12784 let (src_delta, scene_delta) = frontend
12785 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12786 .await
12787 .unwrap();
12788 insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12789 assert_eq!(
12790 scene_delta.new_graph.objects.len(),
12791 7, "{:#?}",
12793 scene_delta.new_graph.objects
12794 );
12795
12796 ctx.close().await;
12797 mock_ctx.close().await;
12798 }
12799
12800 #[tokio::test(flavor = "multi_thread")]
12801 async fn test_diameter_single_arc_segment_with_label_position() {
12802 let initial_source = "\
12803sketch(on = XY) {
12804 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12805}
12806";
12807
12808 let program = Program::parse(initial_source).unwrap().0.unwrap();
12809 let mut frontend = FrontendState::new();
12810 let mock_ctx = ExecutorContext::new_mock(None).await;
12811 let version = Version(0);
12812
12813 frontend.program = program.clone();
12814 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12815 frontend.update_state_after_exec(outcome, true);
12816 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12817 let sketch_id = sketch_object.id;
12818 let sketch = expect_sketch(sketch_object);
12819 let arc_id = sketch
12820 .segments
12821 .iter()
12822 .find(|&seg_id| {
12823 let obj = frontend.scene_graph.objects.get(seg_id.0);
12824 matches!(
12825 obj.map(|o| &o.kind),
12826 Some(ObjectKind::Segment {
12827 segment: Segment::Arc(_)
12828 })
12829 )
12830 })
12831 .unwrap();
12832
12833 let label_position = Point2d {
12834 x: Number {
12835 value: 10.0,
12836 units: NumericSuffix::Mm,
12837 },
12838 y: Number {
12839 value: 11.0,
12840 units: NumericSuffix::Mm,
12841 },
12842 };
12843 let constraint = Constraint::Diameter(Diameter {
12844 arc: *arc_id,
12845 diameter: Number {
12846 value: 10.0,
12847 units: NumericSuffix::Mm,
12848 },
12849 label_position: Some(label_position.clone()),
12850 source: Default::default(),
12851 });
12852 let (src_delta, scene_delta) = frontend
12853 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12854 .await
12855 .unwrap();
12856 insta::assert_snapshot!(
12857 "test_diameter_single_arc_segment_with_label_position",
12858 src_delta.text.as_str()
12859 );
12860
12861 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12862 let sketch = expect_sketch(sketch_object);
12863 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12864 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12865 panic!("Expected constraint object");
12866 };
12867 let Constraint::Diameter(diameter) = constraint else {
12868 panic!("Expected diameter constraint");
12869 };
12870 assert_eq!(diameter.label_position, Some(label_position));
12871
12872 mock_ctx.close().await;
12873 }
12874
12875 #[tokio::test(flavor = "multi_thread")]
12876 async fn test_edit_diameter_constraint_label_position() {
12877 let initial_source = "\
12878sketch(on = XY) {
12879 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12880 diameter(arc1) == 10mm
12881}
12882";
12883
12884 let program = Program::parse(initial_source).unwrap().0.unwrap();
12885 let mut frontend = FrontendState::new();
12886 let mock_ctx = ExecutorContext::new_mock(None).await;
12887 let version = Version(0);
12888
12889 frontend.program = program.clone();
12890 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12891 frontend.update_state_after_exec(outcome, true);
12892 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12893 let sketch_id = sketch_object.id;
12894 let sketch = expect_sketch(sketch_object);
12895 let constraint_id = sketch.constraints[0];
12896 let label_position = Point2d {
12897 x: Number {
12898 value: 10.0,
12899 units: NumericSuffix::Mm,
12900 },
12901 y: Number {
12902 value: 11.0,
12903 units: NumericSuffix::Mm,
12904 },
12905 };
12906
12907 let (src_delta, scene_delta) = frontend
12908 .edit_distance_constraint_label_position(
12909 &mock_ctx,
12910 version,
12911 sketch_id,
12912 constraint_id,
12913 label_position.clone(),
12914 vec![],
12915 )
12916 .await
12917 .unwrap();
12918 insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12919
12920 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12921 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12922 panic!("Expected constraint object");
12923 };
12924 let Constraint::Diameter(diameter) = constraint else {
12925 panic!("Expected diameter constraint");
12926 };
12927 assert_eq!(diameter.label_position, Some(label_position));
12928
12929 mock_ctx.close().await;
12930 }
12931
12932 #[tokio::test(flavor = "multi_thread")]
12933 async fn test_diameter_error_cases() {
12934 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12935 let mock_ctx = ExecutorContext::new_mock(None).await;
12936 let version = Version(0);
12937
12938 let initial_source_point = "\
12940sketch(on = XY) {
12941 point(at = [var 1, var 2])
12942}
12943";
12944 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12945 let mut frontend_point = FrontendState::new();
12946 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12947 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12948 let sketch_id_point = sketch_object_point.id;
12949 let sketch_point = expect_sketch(sketch_object_point);
12950 let point_id = *sketch_point.segments.first().unwrap();
12951
12952 let constraint_point = Constraint::Diameter(Diameter {
12953 arc: point_id,
12954 diameter: Number {
12955 value: 10.0,
12956 units: NumericSuffix::Mm,
12957 },
12958 label_position: None,
12959 source: Default::default(),
12960 });
12961 let result_point = frontend_point
12962 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12963 .await;
12964 assert!(result_point.is_err(), "Single point should error for diameter");
12965
12966 let initial_source_line = "\
12968sketch(on = XY) {
12969 line(start = [var 1, var 2], end = [var 3, var 4])
12970}
12971";
12972 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12973 let mut frontend_line = FrontendState::new();
12974 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12975 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12976 let sketch_id_line = sketch_object_line.id;
12977 let sketch_line = expect_sketch(sketch_object_line);
12978 let line_id = *sketch_line.segments.first().unwrap();
12979
12980 let constraint_line = Constraint::Diameter(Diameter {
12981 arc: line_id,
12982 diameter: Number {
12983 value: 10.0,
12984 units: NumericSuffix::Mm,
12985 },
12986 label_position: None,
12987 source: Default::default(),
12988 });
12989 let result_line = frontend_line
12990 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12991 .await;
12992 assert!(result_line.is_err(), "Single line segment should error for diameter");
12993
12994 ctx.close().await;
12995 mock_ctx.close().await;
12996 }
12997
12998 #[tokio::test(flavor = "multi_thread")]
12999 async fn test_line_horizontal() {
13000 let initial_source = "\
13001sketch(on = XY) {
13002 line(start = [var 1, var 2], end = [var 3, var 4])
13003}
13004";
13005
13006 let program = Program::parse(initial_source).unwrap().0.unwrap();
13007
13008 let mut frontend = FrontendState::new();
13009
13010 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13011 let mock_ctx = ExecutorContext::new_mock(None).await;
13012 let version = Version(0);
13013
13014 frontend.hack_set_program(&ctx, program).await.unwrap();
13015 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13016 let sketch_id = sketch_object.id;
13017 let sketch = expect_sketch(sketch_object);
13018 let line1_id = *sketch.segments.get(2).unwrap();
13019
13020 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13021 let (src_delta, scene_delta) = frontend
13022 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13023 .await
13024 .unwrap();
13025 insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13026 assert_eq!(
13027 scene_delta.new_graph.objects.len(),
13028 6,
13029 "{:#?}",
13030 scene_delta.new_graph.objects
13031 );
13032
13033 ctx.close().await;
13034 mock_ctx.close().await;
13035 }
13036
13037 #[tokio::test(flavor = "multi_thread")]
13038 async fn test_control_point_spline_edge_horizontal() {
13039 let initial_source = "\
13040@settings(experimentalFeatures = allow)
13041splineSketch = sketch(on = XY) {
13042 controlPointSpline1 = controlPointSpline(points = [
13043 [var 0mm, var 0mm],
13044 [var 10mm, var 20mm],
13045 [var 20mm, var 0mm],
13046 ])
13047}
13048";
13049
13050 let program = Program::parse(initial_source).unwrap().0.unwrap();
13051
13052 let mut frontend = FrontendState::new();
13053
13054 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13055 let mock_ctx = ExecutorContext::new_mock(None).await;
13056 let version = Version(0);
13057
13058 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13059 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13060 let sketch_id = sketch_object.id;
13061 let sketch = expect_sketch(sketch_object);
13062 let spline_id = sketch
13063 .segments
13064 .iter()
13065 .copied()
13066 .find(|seg_id| {
13067 matches!(
13068 &frontend.scene_graph.objects[seg_id.0].kind,
13069 ObjectKind::Segment {
13070 segment: Segment::ControlPointSpline(_)
13071 }
13072 )
13073 })
13074 .expect("Expected a control point spline segment in sketch");
13075 let edge_id = frontend
13076 .scene_graph
13077 .objects
13078 .iter()
13079 .find_map(|obj| match &obj.kind {
13080 ObjectKind::Segment {
13081 segment: Segment::Line(line),
13082 } if line.owner == Some(spline_id) => Some(obj.id),
13083 _ => None,
13084 })
13085 .expect("Expected an owned control-polygon edge");
13086
13087 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13088 let (src_delta, _) = frontend
13089 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13090 .await
13091 .unwrap();
13092 assert!(
13093 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13094 "Expected horizontal constraint on spline edge, got: {}",
13095 src_delta.text
13096 );
13097
13098 ctx.close().await;
13099 mock_ctx.close().await;
13100 }
13101
13102 #[tokio::test(flavor = "multi_thread")]
13103 async fn test_control_point_spline_edge_angle() {
13104 let initial_source = "\
13105@settings(experimentalFeatures = allow)
13106splineSketch = sketch(on = XY) {
13107 controlPointSpline1 = controlPointSpline(points = [
13108 [var 0mm, var 0mm],
13109 [var 10mm, var 20mm],
13110 [var 20mm, var 0mm],
13111 ])
13112
13113 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13114}
13115";
13116
13117 let program = Program::parse(initial_source).unwrap().0.unwrap();
13118
13119 let mut frontend = FrontendState::new();
13120
13121 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13122 let mock_ctx = ExecutorContext::new_mock(None).await;
13123 let version = Version(0);
13124
13125 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13126 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13127 let sketch_id = sketch_object.id;
13128 let sketch = expect_sketch(sketch_object);
13129 let spline_id = sketch
13130 .segments
13131 .iter()
13132 .copied()
13133 .find(|seg_id| {
13134 matches!(
13135 &frontend.scene_graph.objects[seg_id.0].kind,
13136 ObjectKind::Segment {
13137 segment: Segment::ControlPointSpline(_)
13138 }
13139 )
13140 })
13141 .expect("Expected a control point spline segment in sketch");
13142 let edge_id = frontend
13143 .scene_graph
13144 .objects
13145 .iter()
13146 .find_map(|obj| match &obj.kind {
13147 ObjectKind::Segment {
13148 segment: Segment::Line(line),
13149 } if line.owner == Some(spline_id) => Some(obj.id),
13150 _ => None,
13151 })
13152 .expect("Expected an owned control-polygon edge");
13153 let line1_id = frontend
13154 .scene_graph
13155 .objects
13156 .iter()
13157 .find_map(|obj| match &obj.kind {
13158 ObjectKind::Segment {
13159 segment: Segment::Line(line),
13160 } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
13161 _ => None,
13162 })
13163 .or_else(|| {
13164 sketch.segments.iter().copied().find(|seg_id| {
13165 matches!(
13166 &frontend.scene_graph.objects[seg_id.0].kind,
13167 ObjectKind::Segment {
13168 segment: Segment::Line(line),
13169 } if line.owner.is_none()
13170 )
13171 })
13172 })
13173 .expect("Expected a standalone line segment in sketch");
13174
13175 let constraint = Constraint::Angle(Angle {
13176 lines: vec![line1_id, edge_id],
13177 angle: Number {
13178 value: 30.0,
13179 units: NumericSuffix::Deg,
13180 },
13181 sector: None,
13182 inverse: None,
13183 label_position: None,
13184 source: Default::default(),
13185 });
13186 let (src_delta, _) = frontend
13187 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13188 .await
13189 .unwrap();
13190 assert!(
13191 src_delta
13192 .text
13193 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13194 "Expected angle constraint on spline edge, got: {}",
13195 src_delta.text
13196 );
13197
13198 ctx.close().await;
13199 mock_ctx.close().await;
13200 }
13201
13202 #[tokio::test(flavor = "multi_thread")]
13203 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13204 let initial_source = "\
13205@settings(experimentalFeatures = allow)
13206splineSketch = sketch(on = XY) {
13207 spline1 = controlPointSpline(points = [
13208 [var 0mm, var 0mm],
13209 [var 10mm, var 20mm],
13210 [var 20mm, var 0mm],
13211 ])
13212 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13213 coincident([spline1.controls[1], spline1.edges[0]])
13214 coincident([spline1.controls[0], line1])
13215}
13216";
13217
13218 let program = Program::parse(initial_source).unwrap().0.unwrap();
13219
13220 let mut frontend = FrontendState::new();
13221
13222 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13223 let mock_ctx = ExecutorContext::new_mock(None).await;
13224
13225 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13226
13227 let ui_scene_graph = frontend.scene_graph_for_ui();
13228 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13229 let sketch = expect_sketch(sketch_object);
13230
13231 assert_eq!(
13232 sketch.constraints.len(),
13233 1,
13234 "Expected only the external coincident constraint to remain visible in the UI scene graph"
13235 );
13236
13237 let visible_constraints = ui_scene_graph
13238 .objects
13239 .iter()
13240 .filter_map(|object| match &object.kind {
13241 ObjectKind::Constraint {
13242 constraint: Constraint::Coincident(coincident),
13243 } => Some(coincident.clone()),
13244 _ => None,
13245 })
13246 .collect::<Vec<_>>();
13247
13248 assert_eq!(
13249 visible_constraints.len(),
13250 1,
13251 "Expected only one coincident constraint object in the UI scene graph"
13252 );
13253 assert_eq!(
13254 visible_constraints[0].get_segments().len(),
13255 2,
13256 "Expected the remaining visible coincident constraint to reference two segments"
13257 );
13258
13259 ctx.close().await;
13260 mock_ctx.close().await;
13261 }
13262
13263 #[tokio::test(flavor = "multi_thread")]
13264 async fn test_edit_control_point_spline_can_append_control_point() {
13265 let initial_source = "\
13266@settings(experimentalFeatures = allow)
13267splineSketch = sketch(on = XY) {
13268 controlPointSpline(points = [
13269 [var 0mm, var 0mm],
13270 [var 10mm, var 20mm],
13271 [var 20mm, var 0mm],
13272 ])
13273}
13274";
13275
13276 let program = Program::parse(initial_source).unwrap().0.unwrap();
13277
13278 let mut frontend = FrontendState::new();
13279
13280 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13281 let mock_ctx = ExecutorContext::new_mock(None).await;
13282 let version = Version(0);
13283
13284 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13285 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13286 let sketch_id = sketch_object.id;
13287 let sketch = expect_sketch(sketch_object);
13288 let spline_id = sketch
13289 .segments
13290 .iter()
13291 .copied()
13292 .find(|seg_id| {
13293 matches!(
13294 &frontend.scene_graph.objects[seg_id.0].kind,
13295 ObjectKind::Segment {
13296 segment: Segment::ControlPointSpline(_)
13297 }
13298 )
13299 })
13300 .expect("Expected a control point spline segment in sketch");
13301
13302 let ctor = ControlPointSplineCtor {
13303 points: vec![
13304 Point2d {
13305 x: Expr::Var(Number {
13306 value: 0.0,
13307 units: NumericSuffix::Mm,
13308 }),
13309 y: Expr::Var(Number {
13310 value: 0.0,
13311 units: NumericSuffix::Mm,
13312 }),
13313 },
13314 Point2d {
13315 x: Expr::Var(Number {
13316 value: 10.0,
13317 units: NumericSuffix::Mm,
13318 }),
13319 y: Expr::Var(Number {
13320 value: 20.0,
13321 units: NumericSuffix::Mm,
13322 }),
13323 },
13324 Point2d {
13325 x: Expr::Var(Number {
13326 value: 20.0,
13327 units: NumericSuffix::Mm,
13328 }),
13329 y: Expr::Var(Number {
13330 value: 0.0,
13331 units: NumericSuffix::Mm,
13332 }),
13333 },
13334 Point2d {
13335 x: Expr::Var(Number {
13336 value: 30.0,
13337 units: NumericSuffix::Mm,
13338 }),
13339 y: Expr::Var(Number {
13340 value: 10.0,
13341 units: NumericSuffix::Mm,
13342 }),
13343 },
13344 ],
13345 construction: None,
13346 };
13347
13348 let segments = vec![ExistingSegmentCtor {
13349 id: spline_id,
13350 ctor: SegmentCtor::ControlPointSpline(ctor),
13351 }];
13352 let (src_delta, scene_delta) = frontend
13353 .edit_segments(&mock_ctx, version, sketch_id, segments)
13354 .await
13355 .unwrap();
13356
13357 assert!(
13358 src_delta.text.contains("[var 30mm, var 10mm]"),
13359 "Expected appended spline control point in source, got: {}",
13360 src_delta.text
13361 );
13362
13363 assert!(
13364 scene_delta.invalidates_ids,
13365 "Expected appending a spline control point to invalidate ids"
13366 );
13367 let updated_spline = scene_delta
13368 .new_graph
13369 .objects
13370 .iter()
13371 .find_map(|obj| match &obj.kind {
13372 ObjectKind::Segment {
13373 segment: Segment::ControlPointSpline(updated_spline),
13374 } if updated_spline.controls.len() == 4 => Some(updated_spline),
13375 _ => None,
13376 })
13377 .expect("Expected edited scene graph to contain a four-point control point spline");
13378 assert_eq!(
13379 updated_spline.controls.len(),
13380 4,
13381 "Expected edited spline to expose four control points"
13382 );
13383
13384 ctx.close().await;
13385 mock_ctx.close().await;
13386 }
13387
13388 #[tokio::test(flavor = "multi_thread")]
13389 async fn test_line_vertical() {
13390 let initial_source = "\
13391sketch(on = XY) {
13392 line(start = [var 1, var 2], end = [var 3, var 4])
13393}
13394";
13395
13396 let program = Program::parse(initial_source).unwrap().0.unwrap();
13397
13398 let mut frontend = FrontendState::new();
13399
13400 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13401 let mock_ctx = ExecutorContext::new_mock(None).await;
13402 let version = Version(0);
13403
13404 frontend.hack_set_program(&ctx, program).await.unwrap();
13405 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13406 let sketch_id = sketch_object.id;
13407 let sketch = expect_sketch(sketch_object);
13408 let line1_id = *sketch.segments.get(2).unwrap();
13409
13410 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13411 let (src_delta, scene_delta) = frontend
13412 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13413 .await
13414 .unwrap();
13415 insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13416 assert_eq!(
13417 scene_delta.new_graph.objects.len(),
13418 6,
13419 "{:#?}",
13420 scene_delta.new_graph.objects
13421 );
13422
13423 ctx.close().await;
13424 mock_ctx.close().await;
13425 }
13426
13427 #[tokio::test(flavor = "multi_thread")]
13428 async fn test_points_vertical() {
13429 let initial_source = "\
13430sketch001 = sketch(on = XY) {
13431 p0 = point(at = [var -2.23mm, var 3.1mm])
13432 pf = point(at = [4, 4])
13433}
13434";
13435
13436 let program = Program::parse(initial_source).unwrap().0.unwrap();
13437
13438 let mut frontend = FrontendState::new();
13439
13440 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13441 let mock_ctx = ExecutorContext::new_mock(None).await;
13442 let version = Version(0);
13443
13444 frontend.hack_set_program(&ctx, program).await.unwrap();
13445 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13446 let sketch_id = sketch_object.id;
13447 let sketch = expect_sketch(sketch_object);
13448 let point_ids = vec![
13449 sketch.segments.first().unwrap().to_owned(),
13450 sketch.segments.get(1).unwrap().to_owned(),
13451 ];
13452
13453 let constraint = Constraint::Vertical(Vertical::Points {
13454 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13455 });
13456 let (src_delta, scene_delta) = frontend
13457 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13458 .await
13459 .unwrap();
13460 insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13461 assert_eq!(
13462 scene_delta.new_graph.objects.len(),
13463 5,
13464 "{:#?}",
13465 scene_delta.new_graph.objects
13466 );
13467
13468 ctx.close().await;
13469 mock_ctx.close().await;
13470 }
13471
13472 #[tokio::test(flavor = "multi_thread")]
13473 async fn test_points_horizontal() {
13474 let initial_source = "\
13475sketch001 = sketch(on = XY) {
13476 p0 = point(at = [var -2.23mm, var 3.1mm])
13477 pf = point(at = [4, 4])
13478}
13479";
13480
13481 let program = Program::parse(initial_source).unwrap().0.unwrap();
13482
13483 let mut frontend = FrontendState::new();
13484
13485 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13486 let mock_ctx = ExecutorContext::new_mock(None).await;
13487 let version = Version(0);
13488
13489 frontend.hack_set_program(&ctx, program).await.unwrap();
13490 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13491 let sketch_id = sketch_object.id;
13492 let sketch = expect_sketch(sketch_object);
13493 let point_ids = vec![
13494 sketch.segments.first().unwrap().to_owned(),
13495 sketch.segments.get(1).unwrap().to_owned(),
13496 ];
13497
13498 let constraint = Constraint::Horizontal(Horizontal::Points {
13499 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13500 });
13501 let (src_delta, scene_delta) = frontend
13502 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13503 .await
13504 .unwrap();
13505 insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13506 assert_eq!(
13507 scene_delta.new_graph.objects.len(),
13508 5,
13509 "{:#?}",
13510 scene_delta.new_graph.objects
13511 );
13512
13513 ctx.close().await;
13514 mock_ctx.close().await;
13515 }
13516
13517 #[tokio::test(flavor = "multi_thread")]
13518 async fn test_point_horizontal_with_origin() {
13519 let initial_source = "\
13520sketch001 = sketch(on = XY) {
13521 p0 = point(at = [var -2.23mm, var 3.1mm])
13522}
13523";
13524
13525 let program = Program::parse(initial_source).unwrap().0.unwrap();
13526
13527 let mut frontend = FrontendState::new();
13528
13529 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13530 let mock_ctx = ExecutorContext::new_mock(None).await;
13531 let version = Version(0);
13532
13533 frontend.hack_set_program(&ctx, program).await.unwrap();
13534 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13535 let sketch_id = sketch_object.id;
13536 let sketch = expect_sketch(sketch_object);
13537 let point_id = *sketch.segments.first().unwrap();
13538
13539 let constraint = Constraint::Horizontal(Horizontal::Points {
13540 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13541 });
13542 let (src_delta, scene_delta) = frontend
13543 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13544 .await
13545 .unwrap();
13546 insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13547 assert_eq!(
13548 scene_delta.new_graph.objects.len(),
13549 4,
13550 "{:#?}",
13551 scene_delta.new_graph.objects
13552 );
13553
13554 ctx.close().await;
13555 mock_ctx.close().await;
13556 }
13557
13558 #[tokio::test(flavor = "multi_thread")]
13559 async fn test_lines_equal_length() {
13560 let initial_source = "\
13561sketch(on = XY) {
13562 line(start = [var 1, var 2], end = [var 3, var 4])
13563 line(start = [var 5, var 6], end = [var 7, var 8])
13564}
13565";
13566
13567 let program = Program::parse(initial_source).unwrap().0.unwrap();
13568
13569 let mut frontend = FrontendState::new();
13570
13571 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13572 let mock_ctx = ExecutorContext::new_mock(None).await;
13573 let version = Version(0);
13574
13575 frontend.hack_set_program(&ctx, program).await.unwrap();
13576 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13577 let sketch_id = sketch_object.id;
13578 let sketch = expect_sketch(sketch_object);
13579 let line1_id = *sketch.segments.get(2).unwrap();
13580 let line2_id = *sketch.segments.get(5).unwrap();
13581
13582 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13583 lines: vec![line1_id, line2_id],
13584 });
13585 let (src_delta, scene_delta) = frontend
13586 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13587 .await
13588 .unwrap();
13589 insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13590 assert_eq!(
13591 scene_delta.new_graph.objects.len(),
13592 9,
13593 "{:#?}",
13594 scene_delta.new_graph.objects
13595 );
13596
13597 ctx.close().await;
13598 mock_ctx.close().await;
13599 }
13600
13601 #[tokio::test(flavor = "multi_thread")]
13602 async fn test_add_constraint_multi_line_equal_length() {
13603 let initial_source = "\
13604sketch(on = XY) {
13605 line(start = [var 1, var 2], end = [var 3, var 4])
13606 line(start = [var 5, var 6], end = [var 7, var 8])
13607 line(start = [var 9, var 10], end = [var 11, var 12])
13608}
13609";
13610
13611 let program = Program::parse(initial_source).unwrap().0.unwrap();
13612
13613 let mut frontend = FrontendState::new();
13614 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13615 let mock_ctx = ExecutorContext::new_mock(None).await;
13616 let version = Version(0);
13617
13618 frontend.hack_set_program(&ctx, program).await.unwrap();
13619 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13620 let sketch_id = sketch_object.id;
13621 let sketch = expect_sketch(sketch_object);
13622 let line1_id = *sketch.segments.get(2).unwrap();
13623 let line2_id = *sketch.segments.get(5).unwrap();
13624 let line3_id = *sketch.segments.get(8).unwrap();
13625
13626 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13627 lines: vec![line1_id, line2_id, line3_id],
13628 });
13629 let (src_delta, scene_delta) = frontend
13630 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13631 .await
13632 .unwrap();
13633 insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13634 let constraints = scene_delta
13635 .new_graph
13636 .objects
13637 .iter()
13638 .filter_map(|obj| {
13639 let ObjectKind::Constraint { constraint } = &obj.kind else {
13640 return None;
13641 };
13642 Some(constraint)
13643 })
13644 .collect::<Vec<_>>();
13645
13646 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13647 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13648 panic!("expected equal length constraint, got {:?}", constraints[0]);
13649 };
13650 assert_eq!(lines_equal_length.lines.len(), 3);
13651
13652 ctx.close().await;
13653 mock_ctx.close().await;
13654 }
13655
13656 #[tokio::test(flavor = "multi_thread")]
13657 async fn test_lines_parallel() {
13658 let initial_source = "\
13659sketch(on = XY) {
13660 line(start = [var 1, var 2], end = [var 3, var 4])
13661 line(start = [var 5, var 6], end = [var 7, var 8])
13662}
13663";
13664
13665 let program = Program::parse(initial_source).unwrap().0.unwrap();
13666
13667 let mut frontend = FrontendState::new();
13668
13669 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13670 let mock_ctx = ExecutorContext::new_mock(None).await;
13671 let version = Version(0);
13672
13673 frontend.hack_set_program(&ctx, program).await.unwrap();
13674 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13675 let sketch_id = sketch_object.id;
13676 let sketch = expect_sketch(sketch_object);
13677 let line1_id = *sketch.segments.get(2).unwrap();
13678 let line2_id = *sketch.segments.get(5).unwrap();
13679
13680 let constraint = Constraint::Parallel(Parallel {
13681 lines: vec![line1_id, line2_id],
13682 });
13683 let (src_delta, scene_delta) = frontend
13684 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13685 .await
13686 .unwrap();
13687 insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13688 assert_eq!(
13689 scene_delta.new_graph.objects.len(),
13690 9,
13691 "{:#?}",
13692 scene_delta.new_graph.objects
13693 );
13694
13695 ctx.close().await;
13696 mock_ctx.close().await;
13697 }
13698
13699 #[tokio::test(flavor = "multi_thread")]
13700 async fn test_lines_parallel_multiline() {
13701 let initial_source = "\
13702sketch(on = XY) {
13703 line(start = [var 1, var 2], end = [var 3, var 4])
13704 line(start = [var 5, var 6], end = [var 7, var 8])
13705 line(start = [var 9, var 10], end = [var 11, var 12])
13706}
13707";
13708
13709 let program = Program::parse(initial_source).unwrap().0.unwrap();
13710
13711 let mut frontend = FrontendState::new();
13712
13713 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13714 let mock_ctx = ExecutorContext::new_mock(None).await;
13715 let version = Version(0);
13716
13717 frontend.hack_set_program(&ctx, program).await.unwrap();
13718 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13719 let sketch_id = sketch_object.id;
13720 let sketch = expect_sketch(sketch_object);
13721 let line1_id = *sketch.segments.get(2).unwrap();
13722 let line2_id = *sketch.segments.get(5).unwrap();
13723 let line3_id = *sketch.segments.get(8).unwrap();
13724
13725 let constraint = Constraint::Parallel(Parallel {
13726 lines: vec![line1_id, line2_id, line3_id],
13727 });
13728 let (src_delta, scene_delta) = frontend
13729 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13730 .await
13731 .unwrap();
13732 insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13733
13734 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13735 let sketch = expect_sketch(sketch_object);
13736 assert_eq!(sketch.constraints.len(), 1);
13737
13738 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13739 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13740 panic!("Expected constraint object");
13741 };
13742 let Constraint::Parallel(parallel) = constraint else {
13743 panic!("Expected parallel constraint");
13744 };
13745 assert_eq!(parallel.lines.len(), 3);
13746
13747 ctx.close().await;
13748 mock_ctx.close().await;
13749 }
13750
13751 #[tokio::test(flavor = "multi_thread")]
13752 async fn test_lines_perpendicular() {
13753 let initial_source = "\
13754sketch(on = XY) {
13755 line(start = [var 1, var 2], end = [var 3, var 4])
13756 line(start = [var 5, var 6], end = [var 7, var 8])
13757}
13758";
13759
13760 let program = Program::parse(initial_source).unwrap().0.unwrap();
13761
13762 let mut frontend = FrontendState::new();
13763
13764 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13765 let mock_ctx = ExecutorContext::new_mock(None).await;
13766 let version = Version(0);
13767
13768 frontend.hack_set_program(&ctx, program).await.unwrap();
13769 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13770 let sketch_id = sketch_object.id;
13771 let sketch = expect_sketch(sketch_object);
13772 let line1_id = *sketch.segments.get(2).unwrap();
13773 let line2_id = *sketch.segments.get(5).unwrap();
13774
13775 let constraint = Constraint::Perpendicular(Perpendicular {
13776 lines: vec![line1_id, line2_id],
13777 });
13778 let (src_delta, scene_delta) = frontend
13779 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13780 .await
13781 .unwrap();
13782 insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13783 assert_eq!(
13784 scene_delta.new_graph.objects.len(),
13785 9,
13786 "{:#?}",
13787 scene_delta.new_graph.objects
13788 );
13789
13790 ctx.close().await;
13791 mock_ctx.close().await;
13792 }
13793
13794 #[tokio::test(flavor = "multi_thread")]
13795 async fn test_lines_angle() {
13796 let initial_source = "\
13797sketch(on = XY) {
13798 line(start = [var 1, var 2], end = [var 3, var 4])
13799 line(start = [var 5, var 6], end = [var 7, var 8])
13800}
13801";
13802
13803 let program = Program::parse(initial_source).unwrap().0.unwrap();
13804
13805 let mut frontend = FrontendState::new();
13806
13807 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13808 let mock_ctx = ExecutorContext::new_mock(None).await;
13809 let version = Version(0);
13810
13811 frontend.hack_set_program(&ctx, program).await.unwrap();
13812 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13813 let sketch_id = sketch_object.id;
13814 let sketch = expect_sketch(sketch_object);
13815 let line1_id = *sketch.segments.get(2).unwrap();
13816 let line2_id = *sketch.segments.get(5).unwrap();
13817
13818 let constraint = Constraint::Angle(Angle {
13819 lines: vec![line1_id, line2_id],
13820 angle: Number {
13821 value: 30.0,
13822 units: NumericSuffix::Deg,
13823 },
13824 sector: None,
13825 inverse: None,
13826 label_position: None,
13827 source: Default::default(),
13828 });
13829 let (src_delta, scene_delta) = frontend
13830 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13831 .await
13832 .unwrap();
13833 insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13834 assert_eq!(
13835 scene_delta.new_graph.objects.len(),
13836 9,
13837 "{:#?}",
13838 scene_delta.new_graph.objects
13839 );
13840
13841 ctx.close().await;
13842 mock_ctx.close().await;
13843 }
13844
13845 #[tokio::test(flavor = "multi_thread")]
13846 async fn test_lines_angle_with_sector_uses_angle_dimension() {
13847 let initial_source = "\
13848sketch(on = XY) {
13849 line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13850 line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13851}
13852";
13853
13854 let program = Program::parse(initial_source).unwrap().0.unwrap();
13855
13856 let mut frontend = FrontendState::new();
13857
13858 let mock_ctx = ExecutorContext::new_mock(None).await;
13859 let version = Version(0);
13860
13861 frontend.program = program.clone();
13862 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13863 frontend.update_state_after_exec(outcome, true);
13864 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13865 let sketch_id = sketch_object.id;
13866 let sketch = expect_sketch(sketch_object);
13867 let line1_id = *sketch.segments.get(2).unwrap();
13868 let line2_id = *sketch.segments.get(5).unwrap();
13869
13870 let constraint = Constraint::Angle(Angle {
13871 lines: vec![line1_id, line2_id],
13872 angle: Number {
13873 value: 270.0,
13874 units: NumericSuffix::Deg,
13875 },
13876 sector: Some(1),
13877 inverse: Some(true),
13878 label_position: Some(Point2d {
13879 x: Number {
13880 value: -0.73,
13881 units: NumericSuffix::Mm,
13882 },
13883 y: Number {
13884 value: 0.75,
13885 units: NumericSuffix::Mm,
13886 },
13887 }),
13888 source: Default::default(),
13889 });
13890 let (src_delta, _) = frontend
13891 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13892 .await
13893 .unwrap();
13894 assert_eq!(
13895 src_delta.text.as_str(),
13896 "\
13897sketch(on = XY) {
13898 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13899 line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13900 angleDimension(
13901 lines = [line1, line2],
13902 sector = 1,
13903 inverse = true,
13904 labelPosition = [-0.73mm, 0.75mm],
13905) == 270deg
13906}
13907"
13908 );
13909
13910 mock_ctx.close().await;
13911 }
13912
13913 #[tokio::test(flavor = "multi_thread")]
13914 async fn test_segments_tangent() {
13915 let initial_source = "\
13916sketch(on = XY) {
13917 line(start = [var 1, var 2], end = [var 3, var 4])
13918 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13919}
13920";
13921
13922 let program = Program::parse(initial_source).unwrap().0.unwrap();
13923
13924 let mut frontend = FrontendState::new();
13925
13926 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13927 let mock_ctx = ExecutorContext::new_mock(None).await;
13928 let version = Version(0);
13929
13930 frontend.hack_set_program(&ctx, program).await.unwrap();
13931 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13932 let sketch_id = sketch_object.id;
13933 let sketch = expect_sketch(sketch_object);
13934 let line1_id = *sketch.segments.get(2).unwrap();
13935 let arc1_id = *sketch.segments.get(6).unwrap();
13936
13937 let constraint = Constraint::Tangent(Tangent {
13938 input: vec![line1_id, arc1_id],
13939 });
13940 let (src_delta, scene_delta) = frontend
13941 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13942 .await
13943 .unwrap();
13944 insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
13945 assert_eq!(
13946 scene_delta.new_graph.objects.len(),
13947 10,
13948 "{:#?}",
13949 scene_delta.new_graph.objects
13950 );
13951
13952 ctx.close().await;
13953 mock_ctx.close().await;
13954 }
13955
13956 #[tokio::test(flavor = "multi_thread")]
13957 async fn test_point_midpoint() {
13958 let initial_source = "\
13959sketch(on = XY) {
13960 point(at = [var 1, var 1])
13961 line(start = [var 0, var 0], end = [var 6, var 4])
13962}
13963";
13964
13965 let program = Program::parse(initial_source).unwrap().0.unwrap();
13966
13967 let mut frontend = FrontendState::new();
13968
13969 let ctx = ExecutorContext::new_mock(None).await;
13970 let version = Version(0);
13971
13972 frontend.program = program.clone();
13973 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13974 frontend.update_state_after_exec(outcome, true);
13975 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13976 let sketch_id = sketch_object.id;
13977 let sketch = expect_sketch(sketch_object);
13978 let point_id = *sketch.segments.first().unwrap();
13979 let line_id = *sketch.segments.get(3).unwrap();
13980
13981 let constraint = Constraint::Midpoint(Midpoint {
13982 point: ConstraintSegment::from(point_id),
13983 segment: line_id,
13984 });
13985 let (src_delta, scene_delta) = frontend
13986 .add_constraint(&ctx, version, sketch_id, constraint)
13987 .await
13988 .unwrap();
13989 insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
13990 assert_eq!(
13991 scene_delta.new_graph.objects.len(),
13992 7,
13993 "{:#?}",
13994 scene_delta.new_graph.objects
13995 );
13996
13997 ctx.close().await;
13998 }
13999
14000 #[tokio::test(flavor = "multi_thread")]
14001 async fn test_segments_symmetric() {
14002 let initial_source = "\
14003sketch(on = XY) {
14004 line(start = [var 0, var 0], end = [var 0, var 4])
14005 line(start = [var 4, var 0], end = [var 4, var 4])
14006 line(start = [var 2, var -1], end = [var 2, var 5])
14007}
14008";
14009
14010 let program = Program::parse(initial_source).unwrap().0.unwrap();
14011
14012 let mut frontend = FrontendState::new();
14013
14014 let ctx = ExecutorContext::new_mock(None).await;
14015 let version = Version(0);
14016
14017 frontend.program = program.clone();
14018 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14019 frontend.update_state_after_exec(outcome, true);
14020 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14021 let sketch_id = sketch_object.id;
14022 let sketch = expect_sketch(sketch_object);
14023 let line1_id = *sketch.segments.get(2).unwrap();
14024 let line2_id = *sketch.segments.get(5).unwrap();
14025 let axis_id = *sketch.segments.get(8).unwrap();
14026
14027 let constraint = Constraint::Symmetric(Symmetric {
14028 input: vec![line1_id, line2_id],
14029 axis: axis_id,
14030 });
14031 let (src_delta, scene_delta) = frontend
14032 .add_constraint(&ctx, version, sketch_id, constraint)
14033 .await
14034 .unwrap();
14035 insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14036 assert_eq!(
14037 scene_delta.new_graph.objects.len(),
14038 12,
14039 "{:#?}",
14040 scene_delta.new_graph.objects
14041 );
14042
14043 ctx.close().await;
14044 }
14045
14046 #[tokio::test(flavor = "multi_thread")]
14047 async fn test_point_arc_midpoint() {
14048 let initial_source = "\
14049sketch(on = XY) {
14050 point(at = [var 6, var 3])
14051 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14052}
14053";
14054
14055 let program = Program::parse(initial_source).unwrap().0.unwrap();
14056
14057 let mut frontend = FrontendState::new();
14058
14059 let ctx = ExecutorContext::new_mock(None).await;
14060 let version = Version(0);
14061
14062 frontend.program = program.clone();
14063 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14064 frontend.update_state_after_exec(outcome, true);
14065 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14066 let sketch_id = sketch_object.id;
14067 let sketch = expect_sketch(sketch_object);
14068 let point_id = *sketch.segments.first().unwrap();
14069 let arc_id = *sketch.segments.get(4).unwrap();
14070
14071 let constraint = Constraint::Midpoint(Midpoint {
14072 point: ConstraintSegment::from(point_id),
14073 segment: arc_id,
14074 });
14075 let (src_delta, scene_delta) = frontend
14076 .add_constraint(&ctx, version, sketch_id, constraint)
14077 .await
14078 .unwrap();
14079 insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14080 assert_eq!(
14081 scene_delta.new_graph.objects.len(),
14082 8,
14083 "{:#?}",
14084 scene_delta.new_graph.objects
14085 );
14086
14087 ctx.close().await;
14088 }
14089
14090 #[tokio::test(flavor = "multi_thread")]
14091 async fn test_origin_line_midpoint() {
14092 let initial_source = "\
14093sketch(on = XY) {
14094 line(start = [var 0, var 0], end = [var 6, var 4])
14095}
14096";
14097
14098 let program = Program::parse(initial_source).unwrap().0.unwrap();
14099
14100 let mut frontend = FrontendState::new();
14101
14102 let ctx = ExecutorContext::new_mock(None).await;
14103 let version = Version(0);
14104
14105 frontend.program = program.clone();
14106 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14107 frontend.update_state_after_exec(outcome, true);
14108 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14109 let sketch_id = sketch_object.id;
14110 let sketch = expect_sketch(sketch_object);
14111 let line_id = *sketch.segments.get(2).unwrap();
14112
14113 let constraint = Constraint::Midpoint(Midpoint {
14114 point: ConstraintSegment::ORIGIN,
14115 segment: line_id,
14116 });
14117 let (src_delta, scene_delta) = frontend
14118 .add_constraint(&ctx, version, sketch_id, constraint)
14119 .await
14120 .unwrap();
14121 insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14122 assert_eq!(
14123 scene_delta.new_graph.objects.len(),
14124 6,
14125 "{:#?}",
14126 scene_delta.new_graph.objects
14127 );
14128
14129 ctx.close().await;
14130 }
14131
14132 #[tokio::test(flavor = "multi_thread")]
14133 async fn test_origin_arc_midpoint() {
14134 let initial_source = "\
14135sketch(on = XY) {
14136 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14137}
14138";
14139
14140 let program = Program::parse(initial_source).unwrap().0.unwrap();
14141
14142 let mut frontend = FrontendState::new();
14143
14144 let ctx = ExecutorContext::new_mock(None).await;
14145 let version = Version(0);
14146
14147 frontend.program = program.clone();
14148 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14149 frontend.update_state_after_exec(outcome, true);
14150 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14151 let sketch_id = sketch_object.id;
14152 let sketch = expect_sketch(sketch_object);
14153 let arc_id = *sketch.segments.get(3).unwrap();
14154
14155 let constraint = Constraint::Midpoint(Midpoint {
14156 point: ConstraintSegment::ORIGIN,
14157 segment: arc_id,
14158 });
14159 let (src_delta, scene_delta) = frontend
14160 .add_constraint(&ctx, version, sketch_id, constraint)
14161 .await
14162 .unwrap();
14163 insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14164 assert_eq!(
14165 scene_delta.new_graph.objects.len(),
14166 7,
14167 "{:#?}",
14168 scene_delta.new_graph.objects
14169 );
14170
14171 ctx.close().await;
14172 }
14173
14174 #[tokio::test(flavor = "multi_thread")]
14175 async fn test_segments_symmetric_arcs() {
14176 let initial_source = "\
14177sketch(on = XY) {
14178 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14179 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14180 line(start = [var 0, var -10], end = [var 0, var 10])
14181}
14182";
14183
14184 let program = Program::parse(initial_source).unwrap().0.unwrap();
14185
14186 let mut frontend = FrontendState::new();
14187
14188 let ctx = ExecutorContext::new_mock(None).await;
14189 let version = Version(0);
14190
14191 frontend.program = program.clone();
14192 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14193 frontend.update_state_after_exec(outcome, true);
14194 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14195 let sketch_id = sketch_object.id;
14196 let sketch = expect_sketch(sketch_object);
14197 let arc1_id = *sketch.segments.get(3).unwrap();
14198 let arc2_id = *sketch.segments.get(7).unwrap();
14199 let axis_id = *sketch.segments.get(10).unwrap();
14200
14201 let constraint = Constraint::Symmetric(Symmetric {
14202 input: vec![arc1_id, arc2_id],
14203 axis: axis_id,
14204 });
14205 let (src_delta, scene_delta) = frontend
14206 .add_constraint(&ctx, version, sketch_id, constraint)
14207 .await
14208 .unwrap();
14209 insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14210 assert_eq!(
14211 scene_delta.new_graph.objects.len(),
14212 14,
14213 "{:#?}",
14214 scene_delta.new_graph.objects
14215 );
14216
14217 ctx.close().await;
14218 }
14219
14220 #[tokio::test(flavor = "multi_thread")]
14221 async fn test_sketch_on_face_simple() {
14222 let initial_source = "\
14223len = 2mm
14224cube = startSketchOn(XY)
14225 |> startProfile(at = [0, 0])
14226 |> line(end = [len, 0], tag = $side)
14227 |> line(end = [0, len])
14228 |> line(end = [-len, 0])
14229 |> line(end = [0, -len])
14230 |> close()
14231 |> extrude(length = len)
14232
14233face = faceOf(cube, face = side)
14234";
14235
14236 let program = Program::parse(initial_source).unwrap().0.unwrap();
14237
14238 let mut frontend = FrontendState::new();
14239
14240 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14241 let mock_ctx = ExecutorContext::new_mock(None).await;
14242 let version = Version(0);
14243
14244 frontend.hack_set_program(&ctx, program).await.unwrap();
14245 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14246 let face_id = face_object.id;
14247
14248 let sketch_args = SketchCtor {
14249 on: Plane::Object(face_id),
14250 };
14251 let (_src_delta, scene_delta, sketch_id) = frontend
14252 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14253 .await
14254 .unwrap();
14255 assert_eq!(sketch_id, ObjectId(2));
14256 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14257 let sketch_object = &scene_delta.new_graph.objects[2];
14258 assert_eq!(sketch_object.id, ObjectId(2));
14259 assert_eq!(
14260 sketch_object.kind,
14261 ObjectKind::Sketch(Sketch {
14262 args: SketchCtor {
14263 on: Plane::Object(face_id),
14264 },
14265 plane: face_id,
14266 segments: vec![],
14267 constraints: vec![],
14268 })
14269 );
14270 assert_eq!(scene_delta.new_graph.objects.len(), 8);
14271
14272 ctx.close().await;
14273 mock_ctx.close().await;
14274 }
14275
14276 #[tokio::test(flavor = "multi_thread")]
14277 async fn test_new_sketch_on_primitive_index_face() {
14278 let initial_source = "\
14279@settings(kclVersion = 2.0)
14280
14281sketch001 = sketch(on = XY) {
14282 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14283}
14284extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14285shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14286 let program = Program::parse(initial_source).unwrap().0.unwrap();
14287 let ctx = ExecutorContext::new_mock(None).await;
14288 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14289 let solid_id = match outcome.variables.get("shell001") {
14290 Some(KclValueView::Solid { value }) => value.id,
14291 value => panic!("expected shell001 to be a solid, got {value:?}"),
14292 };
14293 let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14294
14295 let mut ast = program.ast;
14296 let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14297 let face_expr = sketch_on_ast_expr(
14298 &mut ast,
14299 &scene_graph,
14300 &solid_references,
14301 &Plane::PrimitiveFace(crate::frontend::api::PrimitiveFacePlane { solid_id, index: 6 }),
14302 )
14303 .unwrap();
14304 let face_decl = ast::VariableDeclaration::new(
14305 ast::VariableDeclarator::new("face001", face_expr),
14306 ast::ItemVisibility::Default,
14307 ast::VariableKind::Const,
14308 );
14309 ast.body
14310 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14311 face_decl,
14312 ))));
14313 let face_source = source_from_ast(&ast);
14314 let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14315 insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14316
14317 let program = Program::parse(&new_source).unwrap().0.unwrap();
14318 ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14319 ctx.close().await;
14320 }
14321
14322 #[tokio::test(flavor = "multi_thread")]
14323 async fn test_sketch_on_wall_artifact_from_region_extrude() {
14324 let initial_source = "\
14325s = sketch(on = YZ) {
14326 line1 = line(start = [0, 0], end = [0, 1])
14327 line2 = line(start = [0, 1], end = [1, 1])
14328 line3 = line(start = [1, 1], end = [0, 0])
14329}
14330region001 = region(point = [0.1, 0.1], sketch = s)
14331extrude001 = extrude(region001, length = 5)
14332";
14333
14334 let program = Program::parse(initial_source).unwrap().0.unwrap();
14335
14336 let mut frontend = FrontendState::new();
14337 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14338 let version = Version(0);
14339
14340 frontend.hack_set_program(&ctx, program).await.unwrap();
14341 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14342
14343 let sketch_args = SketchCtor {
14344 on: Plane::Object(wall_object_id),
14345 };
14346 let (src_delta, _scene_delta, _sketch_id) = frontend
14347 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14348 .await
14349 .unwrap();
14350 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14351
14352 ctx.close().await;
14353 }
14354
14355 #[tokio::test(flavor = "multi_thread")]
14356 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14357 let initial_source = "\
14358sketch001 = sketch(on = YZ) {
14359 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14360 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14361 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14362 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14363 coincident([line1.end, line2.start])
14364 coincident([line2.end, line3.start])
14365 coincident([line3.end, line4.start])
14366 coincident([line4.end, line1.start])
14367 parallel([line2, line4])
14368 parallel([line3, line1])
14369 perpendicular([line1, line2])
14370 horizontal(line3)
14371 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14372}
14373region001 = region(point = [3.1, 3.74], sketch = sketch001)
14374extrude001 = extrude(region001, length = 5)
14375";
14376
14377 let program = Program::parse(initial_source).unwrap().0.unwrap();
14378
14379 let mut frontend = FrontendState::new();
14380 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14381 let version = Version(0);
14382
14383 frontend.hack_set_program(&ctx, program).await.unwrap();
14384 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14385
14386 let sketch_args = SketchCtor {
14387 on: Plane::Object(wall_object_id),
14388 };
14389 let (src_delta, _scene_delta, _sketch_id) = frontend
14390 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14391 .await
14392 .unwrap();
14393 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14394
14395 ctx.close().await;
14396 }
14397
14398 #[tokio::test(flavor = "multi_thread")]
14399 async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14400 let initial_source = "\
14401@settings(kclVersion = 2.0)
14402
14403sketch001 = sketch(on = XY) {
14404 circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14405 circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14406}
14407hidden001 = hide(sketch001)
14408region001 = region(segments = [sketch001.circle2])
14409region002 = region(segments = [sketch001.circle1])
14410extrude001 = extrude([region001, region002], length = 5)
14411";
14412
14413 let program = Program::parse(initial_source).unwrap().0.unwrap();
14414 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14415 let version = Version(0);
14416
14417 for (solid_output_index, expected_face) in [
14418 (0, "faceOf(extrude001[0], face = END)"),
14419 (1, "faceOf(extrude001[1], face = END)"),
14420 ] {
14421 let mut frontend = FrontendState::new();
14422 frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14423 let cap_object_id = find_cap_object_id_with_solid_output_index(
14424 &frontend.scene_graph,
14425 crate::frontend::api::CapKind::End,
14426 solid_output_index,
14427 )
14428 .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14429
14430 let sketch_args = SketchCtor {
14431 on: Plane::Object(cap_object_id),
14432 };
14433 let (src_delta, _scene_delta, _sketch_id) = frontend
14434 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14435 .await
14436 .unwrap();
14437
14438 assert!(
14439 src_delta.text.contains(expected_face),
14440 "expected `{expected_face}` in:\n{}",
14441 src_delta.text
14442 );
14443 assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14444 }
14445
14446 ctx.close().await;
14447 }
14448
14449 #[tokio::test(flavor = "multi_thread")]
14450 async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14451 let initial_source = "\
14452@settings(kclVersion = 2.0)
14453
14454sketch001 = sketch(on = XY) {
14455 rect1Line1 = line(start = [0, 0], end = [1, 0])
14456 rect1Line2 = line(start = [1, 0], end = [1, 1])
14457 rect1Line3 = line(start = [1, 1], end = [0, 1])
14458 rect1Line4 = line(start = [0, 1], end = [0, 0])
14459 rect2Line1 = line(start = [3, 0], end = [4, 0])
14460 rect2Line2 = line(start = [4, 0], end = [4, 1])
14461 rect2Line3 = line(start = [4, 1], end = [3, 1])
14462 rect2Line4 = line(start = [3, 1], end = [3, 0])
14463}
14464hidden001 = hide(sketch001)
14465region001 = region(segments = [
14466 sketch001.rect1Line4,
14467 sketch001.rect1Line1
14468])
14469region002 = region(segments = [
14470 sketch001.rect2Line4,
14471 sketch001.rect2Line1
14472])
14473extrude001 = extrude([region001, region002], length = 5)
14474";
14475
14476 let program = Program::parse(initial_source).unwrap().0.unwrap();
14477 let mut frontend = FrontendState::new();
14478 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14479 let version = Version(0);
14480
14481 frontend.hack_set_program(&ctx, program).await.unwrap();
14482 let region_call = "\
14483region(segments = [
14484 sketch001.rect1Line4,
14485 sketch001.rect1Line1
14486])";
14487 let region_call_start = initial_source.find(region_call).unwrap();
14488 let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14489 let segment_call = "line(start = [0, 0], end = [1, 0])";
14490 let segment_call_start = initial_source.find(segment_call).unwrap();
14491 let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14492 let wall_object_id = frontend
14493 .scene_graph
14494 .objects
14495 .iter()
14496 .find_map(|object| match &object.kind {
14497 ObjectKind::Wall(wall)
14498 if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14499 && wall.source.segment.range == segment_range =>
14500 {
14501 Some(object.id)
14502 }
14503 _ => None,
14504 })
14505 .expect("expected a wall object for region001.tags.rect1Line1");
14506
14507 let sketch_args = SketchCtor {
14508 on: Plane::Object(wall_object_id),
14509 };
14510 let (src_delta, _scene_delta, _sketch_id) = frontend
14511 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14512 .await
14513 .unwrap();
14514
14515 let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14516 assert!(
14517 src_delta.text.contains(expected_face),
14518 "expected `{expected_face}` in:\n{}",
14519 src_delta.text
14520 );
14521 assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14522
14523 ctx.close().await;
14524 }
14525
14526 #[test]
14527 fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14528 let source = "\
14529sketch001 = sketch(on = XY) {
14530 line(start = [0, 0], end = [1, 0])
14531}
14532part = subtract(boxSolid, tools = [cutSolid])
14533 |> appearance(color = \"#8f96a3\")
14534";
14535 let ast = Program::parse(source).unwrap().0.unwrap().ast;
14536 let line_start = source.find("line").unwrap();
14537 let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14538 let line_ref = SourceRef::Simple {
14539 range: [line_start, line_end, 0].into(),
14540 node_path: None,
14541 };
14542 assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14543
14544 let subtract_start = source.find("subtract").unwrap();
14545 let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14546 let subtract_ref = SourceRef::Simple {
14547 range: [subtract_start, subtract_end, 0].into(),
14548 node_path: None,
14549 };
14550 assert_eq!(
14551 variable_name_containing_source_ref(&ast, &subtract_ref),
14552 Some("part".to_owned())
14553 );
14554 }
14555
14556 #[tokio::test(flavor = "multi_thread")]
14557 async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14558 clear_mem_cache().await;
14559 let source = "\
14560boxSolid = startSketchOn(XY)
14561 |> startProfile(at = [0, 0])
14562 |> line(end = [4, 0], tag = $bottomEdge)
14563 |> line(end = [0, 4])
14564 |> line(end = [-4, 0])
14565 |> close()
14566 |> extrude(length = 10)
14567cutSolid = startSketchOn(XY)
14568 |> startProfile(at = [1, 1])
14569 |> line(end = [1, 0])
14570 |> line(end = [0, 1])
14571 |> line(end = [-1, 0])
14572 |> close()
14573 |> extrude(length = 10)
14574part = subtract(boxSolid, tools = [cutSolid])
14575 |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14576";
14577 let program = Program::parse(source).unwrap().0.unwrap();
14578 let mut frontend = FrontendState::new();
14579 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14580 match frontend.hack_set_program(&ctx, program).await.unwrap() {
14581 SetProgramOutcome::Success { .. } => {}
14582 SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14583 }
14584
14585 let sweep_call_start = source.find("extrude").unwrap();
14586 let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14587 let part_call_start = source.find("subtract").unwrap();
14588 let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14589 let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14590 let composite_range = [part_call_start, part_call_end, 0].into();
14591
14592 let cap_object = frontend
14593 .scene_graph
14594 .objects
14595 .iter()
14596 .find(|object| {
14597 matches!(
14598 &object.kind,
14599 ObjectKind::Cap(crate::frontend::api::Cap {
14600 kind: crate::frontend::api::CapKind::End,
14601 source,
14602 ..
14603 }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14604 )
14605 })
14606 .expect("expected end cap object to trace through subtract and original extrude");
14607
14608 let mut ast = frontend.program.ast.clone();
14609 let cap_expr = sketch_on_ast_expr(
14610 &mut ast,
14611 &frontend.scene_graph,
14612 &frontend.solid_references,
14613 &Plane::Object(cap_object.id),
14614 )
14615 .unwrap();
14616 let cap_face_decl = ast::VariableDeclaration::new(
14617 ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14618 ast::ItemVisibility::Default,
14619 ast::VariableKind::Const,
14620 );
14621 ast.body
14622 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14623 cap_face_decl,
14624 ))));
14625 let generated_source = source_from_ast(&ast);
14626
14627 assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14628 assert!(!generated_source.contains("faceOf(boxSolid"));
14629 let ast::Expr::CallExpressionKw(call) = cap_expr else {
14630 panic!("expected faceOf call");
14631 };
14632 assert_eq!(call.callee.name.name, "faceOf");
14633 let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14634 panic!("expected solid name");
14635 };
14636 assert_eq!(solid_name.name.name, "part");
14637 let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14638 panic!("expected face name");
14639 };
14640 assert_eq!(face_name.name.name, "END");
14641
14642 ctx.close().await;
14643 }
14644
14645 #[tokio::test(flavor = "multi_thread")]
14646 async fn test_sketch_on_plane_incremental() {
14647 let initial_source = "\
14648len = 2mm
14649cube = startSketchOn(XY)
14650 |> startProfile(at = [0, 0])
14651 |> line(end = [len, 0], tag = $side)
14652 |> line(end = [0, len])
14653 |> line(end = [-len, 0])
14654 |> line(end = [0, -len])
14655 |> close()
14656 |> extrude(length = len)
14657
14658plane = planeOf(cube, face = side)
14659";
14660
14661 let program = Program::parse(initial_source).unwrap().0.unwrap();
14662
14663 let mut frontend = FrontendState::new();
14664
14665 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14666 let mock_ctx = ExecutorContext::new_mock(None).await;
14667 let version = Version(0);
14668
14669 frontend.hack_set_program(&ctx, program).await.unwrap();
14670 let plane_object = frontend
14672 .scene_graph
14673 .objects
14674 .iter()
14675 .rev()
14676 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14677 .unwrap();
14678 let plane_id = plane_object.id;
14679
14680 let sketch_args = SketchCtor {
14681 on: Plane::Object(plane_id),
14682 };
14683 let (src_delta, scene_delta, sketch_id) = frontend
14684 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14685 .await
14686 .unwrap();
14687 insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14688 assert_eq!(sketch_id, ObjectId(2));
14689 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14690 let sketch_object = &scene_delta.new_graph.objects[2];
14691 assert_eq!(sketch_object.id, ObjectId(2));
14692 assert_eq!(
14693 sketch_object.kind,
14694 ObjectKind::Sketch(Sketch {
14695 args: SketchCtor {
14696 on: Plane::Object(plane_id),
14697 },
14698 plane: plane_id,
14699 segments: vec![],
14700 constraints: vec![],
14701 })
14702 );
14703 assert_eq!(scene_delta.new_graph.objects.len(), 9);
14704
14705 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14706 assert_eq!(plane_object.id, plane_id);
14707 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14708
14709 ctx.close().await;
14710 mock_ctx.close().await;
14711 }
14712
14713 #[tokio::test(flavor = "multi_thread")]
14714 async fn test_new_sketch_uses_unique_variable_name() {
14715 let initial_source = "\
14716sketch1 = sketch(on = XY) {
14717}
14718";
14719
14720 let program = Program::parse(initial_source).unwrap().0.unwrap();
14721
14722 let mut frontend = FrontendState::new();
14723 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14724 let version = Version(0);
14725
14726 frontend.hack_set_program(&ctx, program).await.unwrap();
14727
14728 let sketch_args = SketchCtor {
14729 on: Plane::Default(PlaneName::Yz),
14730 };
14731 let (src_delta, _, _) = frontend
14732 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14733 .await
14734 .unwrap();
14735
14736 insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14737
14738 ctx.close().await;
14739 }
14740
14741 #[tokio::test(flavor = "multi_thread")]
14742 async fn test_new_sketch_twice_using_same_plane() {
14743 let initial_source = "\
14744sketch1 = sketch(on = XY) {
14745}
14746";
14747
14748 let program = Program::parse(initial_source).unwrap().0.unwrap();
14749
14750 let mut frontend = FrontendState::new();
14751 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14752 let version = Version(0);
14753
14754 frontend.hack_set_program(&ctx, program).await.unwrap();
14755
14756 let sketch_args = SketchCtor {
14757 on: Plane::Default(PlaneName::Xy),
14758 };
14759 let (src_delta, _, _) = frontend
14760 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14761 .await
14762 .unwrap();
14763
14764 insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14765
14766 ctx.close().await;
14767 }
14768
14769 #[tokio::test(flavor = "multi_thread")]
14770 async fn test_sketch_mode_reuses_cached_on_expression() {
14771 let initial_source = "\
14772width = 2mm
14773sketch(on = offsetPlane(XY, offset = width)) {
14774 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14775 distance([line1.start, line1.end]) == width
14776}
14777";
14778 let program = Program::parse(initial_source).unwrap().0.unwrap();
14779
14780 let mut frontend = FrontendState::new();
14781 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14782 let mock_ctx = ExecutorContext::new_mock(None).await;
14783 let version = Version(0);
14784 let project_id = ProjectId(0);
14785 let file_id = FileId(0);
14786
14787 frontend.hack_set_program(&ctx, program).await.unwrap();
14788 let initial_object_count = frontend.scene_graph.objects.len();
14789 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14790 .expect("Expected sketch object to exist")
14791 .id;
14792
14793 let scene_delta = frontend
14796 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14797 .await
14798 .unwrap();
14799 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14800
14801 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14804 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14805
14806 ctx.close().await;
14807 mock_ctx.close().await;
14808 }
14809
14810 #[tokio::test(flavor = "multi_thread")]
14811 async fn test_edit_sketch_nested_in_pipe() {
14812 clear_mem_cache().await;
14813 let source = r#"
14814profile = sketch(on = XY) {
14815 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14816}
14817 |> translate(x = 2mm)
14818"#;
14819 let program = Program::parse_no_errs(source).unwrap();
14820 let mut frontend = FrontendState::new();
14821 let mock_ctx = ExecutorContext::new_mock(None).await;
14822 let version = Version(0);
14823
14824 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14825 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14826 .expect("Expected piped sketch object")
14827 .id;
14828
14829 let scene_delta = frontend
14830 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14831 .await
14832 .unwrap();
14833 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14834 assert!(
14835 scene_delta
14836 .new_graph
14837 .objects
14838 .iter()
14839 .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14840 "Expected the piped sketch's segments to be present in sketch mode"
14841 );
14842
14843 clear_mem_cache().await;
14844 mock_ctx.close().await;
14845 }
14846
14847 #[tokio::test(flavor = "multi_thread")]
14848 async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14849 clear_mem_cache().await;
14850 let source = r#"
14851useFirstProfile = true
14852
14853profile = if useFirstProfile {
14854 sketch(on = XY) {
14855 line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14856 }
14857} else {
14858 sketch(on = XY) {
14859 line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14860 }
14861}
14862"#;
14863 let program = Program::parse_no_errs(source).unwrap();
14864 let mut frontend = FrontendState::new();
14865 let mock_ctx = ExecutorContext::new_mock(None).await;
14866 let version = Version(0);
14867
14868 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14869 let sketch_object =
14870 find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14871 let sketch_id = sketch_object.id;
14872 let sketch = expect_sketch(sketch_object);
14873 let line_end_id = *sketch
14874 .segments
14875 .get(1)
14876 .expect("Expected the active branch's line end point");
14877
14878 let scene_delta = frontend
14879 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14880 .await
14881 .unwrap();
14882 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14883
14884 let segments = vec![ExistingSegmentCtor {
14885 id: line_end_id,
14886 ctor: SegmentCtor::Point(PointCtor {
14887 position: Point2d {
14888 x: Expr::Var(Number {
14889 value: 30.0,
14890 units: NumericSuffix::Mm,
14891 }),
14892 y: Expr::Var(Number {
14893 value: 15.0,
14894 units: NumericSuffix::Mm,
14895 }),
14896 },
14897 }),
14898 }];
14899 let (source_delta, _) = frontend
14900 .edit_segments(&mock_ctx, version, sketch_id, segments)
14901 .await
14902 .unwrap();
14903 assert!(
14904 source_delta
14905 .text
14906 .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14907 "Expected the active branch's dragged variables to be updated:\n{}",
14908 source_delta.text
14909 );
14910 assert!(
14911 source_delta
14912 .text
14913 .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14914 "Expected the inactive branch to remain unchanged:\n{}",
14915 source_delta.text
14916 );
14917
14918 clear_mem_cache().await;
14919 mock_ctx.close().await;
14920 }
14921
14922 #[tokio::test(flavor = "multi_thread")]
14923 async fn test_multiple_sketch_blocks() {
14924 let initial_source = "\
14925// Cube that requires the engine.
14926width = 2
14927sketch001 = startSketchOn(XY)
14928profile001 = startProfile(sketch001, at = [0, 0])
14929 |> yLine(length = width, tag = $seg1)
14930 |> xLine(length = width)
14931 |> yLine(length = -width)
14932 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14933 |> close()
14934extrude001 = extrude(profile001, length = width)
14935
14936// Get a value that requires the engine.
14937x = segLen(seg1)
14938
14939// Triangle with side length 2*x.
14940sketch(on = XY) {
14941 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14942 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14943 coincident([line1.end, line2.start])
14944 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14945 coincident([line2.end, line3.start])
14946 coincident([line3.end, line1.start])
14947 equalLength([line3, line1])
14948 equalLength([line1, line2])
14949 distance([line1.start, line1.end]) == 2*x
14950}
14951
14952// Line segment with length x.
14953sketch2 = sketch(on = XY) {
14954 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14955 distance([line1.start, line1.end]) == x
14956}
14957";
14958
14959 let program = Program::parse(initial_source).unwrap().0.unwrap();
14960
14961 let mut frontend = FrontendState::new();
14962
14963 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14964 let mock_ctx = ExecutorContext::new_mock(None).await;
14965 let version = Version(0);
14966 let project_id = ProjectId(0);
14967 let file_id = FileId(0);
14968
14969 frontend.hack_set_program(&ctx, program).await.unwrap();
14970 let sketch_objects = frontend
14971 .scene_graph
14972 .objects
14973 .iter()
14974 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14975 .collect::<Vec<_>>();
14976 let sketch1_id = sketch_objects.first().unwrap().id;
14977 let sketch2_id = sketch_objects.get(1).unwrap().id;
14978 let point1_id = ObjectId(sketch1_id.0 + 1);
14980 let point2_id = ObjectId(sketch2_id.0 + 1);
14982
14983 let scene_delta = frontend
14992 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14993 .await
14994 .unwrap();
14995 assert_eq!(
14996 scene_delta.new_graph.objects.len(),
14997 18,
14998 "{:#?}",
14999 scene_delta.new_graph.objects
15000 );
15001
15002 let point_ctor = PointCtor {
15004 position: Point2d {
15005 x: Expr::Var(Number {
15006 value: 1.0,
15007 units: NumericSuffix::Mm,
15008 }),
15009 y: Expr::Var(Number {
15010 value: 2.0,
15011 units: NumericSuffix::Mm,
15012 }),
15013 },
15014 };
15015 let segments = vec![ExistingSegmentCtor {
15016 id: point1_id,
15017 ctor: SegmentCtor::Point(point_ctor),
15018 }];
15019 let (src_delta, _) = frontend
15020 .edit_segments(&mock_ctx, version, sketch1_id, segments)
15021 .await
15022 .unwrap();
15023 insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15025 let edited_sketch1_source = src_delta.text.clone();
15026
15027 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15029 assert_eq!(src_delta.text, edited_sketch1_source);
15030 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15038 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15039
15040 let scene_delta = frontend
15048 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15049 .await
15050 .unwrap();
15051 assert_eq!(
15052 scene_delta.new_graph.objects.len(),
15053 24,
15054 "{:#?}",
15055 scene_delta.new_graph.objects
15056 );
15057
15058 let point_ctor = PointCtor {
15060 position: Point2d {
15061 x: Expr::Var(Number {
15062 value: 3.0,
15063 units: NumericSuffix::Mm,
15064 }),
15065 y: Expr::Var(Number {
15066 value: 4.0,
15067 units: NumericSuffix::Mm,
15068 }),
15069 },
15070 };
15071 let segments = vec![ExistingSegmentCtor {
15072 id: point2_id,
15073 ctor: SegmentCtor::Point(point_ctor),
15074 }];
15075 let (src_delta, _) = frontend
15076 .edit_segments(&mock_ctx, version, sketch2_id, segments)
15077 .await
15078 .unwrap();
15079 insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15081 let edited_sketch2_source = src_delta.text.clone();
15082
15083 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15085 assert_eq!(src_delta.text, edited_sketch2_source);
15086
15087 ctx.close().await;
15088 mock_ctx.close().await;
15089 }
15090
15091 #[tokio::test(flavor = "multi_thread")]
15092 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15093 clear_mem_cache().await;
15094
15095 let source = r#"sketch001 = sketch(on = XZ) {
15096 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15097}
15098sketch002 = sketch(on = XY) {
15099 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15100 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15101 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15102 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15103 coincident([line1.end, line2.start])
15104 coincident([line2.end, line3.start])
15105 coincident([line3.end, line4.start])
15106 coincident([line4.end, line1.start])
15107 parallel([line2, line4])
15108 parallel([line3, line1])
15109 perpendicular([line1, line2])
15110 horizontal(line3)
15111 coincident([line1.start, ORIGIN])
15112}
15113"#;
15114
15115 let program = Program::parse(source).unwrap().0.unwrap();
15116 let mut frontend = FrontendState::new();
15117 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15118 let mock_ctx = ExecutorContext::new_mock(None).await;
15119 let version = Version(0);
15120 let project_id = ProjectId(0);
15121 let file_id = FileId(0);
15122
15123 frontend.hack_set_program(&ctx, program).await.unwrap();
15124 let sketch_objects = frontend
15125 .scene_graph
15126 .objects
15127 .iter()
15128 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15129 .collect::<Vec<_>>();
15130 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15131
15132 let sketch1_id = sketch_objects[0].id;
15133 let sketch2_id = sketch_objects[1].id;
15134
15135 frontend
15136 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15137 .await
15138 .unwrap();
15139 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15140
15141 let scene_delta = frontend
15142 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15143 .await
15144 .unwrap();
15145 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15146
15147 clear_mem_cache().await;
15148 ctx.close().await;
15149 mock_ctx.close().await;
15150 }
15151
15152 #[tokio::test(flavor = "multi_thread")]
15157 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15158 let initial_source = "@settings(defaultLengthUnit = mm)
15160
15161sketch001 = sketch(on = XY) {
15162 point(at = [1in, 2in])
15163}
15164";
15165
15166 let program = Program::parse(initial_source).unwrap().0.unwrap();
15167 let mut frontend = FrontendState::new();
15168
15169 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15170 let mock_ctx = ExecutorContext::new_mock(None).await;
15171 let version = Version(0);
15172 let project_id = ProjectId(0);
15173 let file_id = FileId(0);
15174
15175 frontend.hack_set_program(&ctx, program).await.unwrap();
15176 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15177 let sketch_id = sketch_object.id;
15178
15179 frontend
15181 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15182 .await
15183 .unwrap();
15184
15185 let point_ctor = PointCtor {
15187 position: Point2d {
15188 x: Expr::Number(Number {
15189 value: 5.0,
15190 units: NumericSuffix::Mm,
15191 }),
15192 y: Expr::Number(Number {
15193 value: 6.0,
15194 units: NumericSuffix::Mm,
15195 }),
15196 },
15197 };
15198 let segment = SegmentCtor::Point(point_ctor);
15199 let (src_delta, scene_delta) = frontend
15200 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15201 .await
15202 .unwrap();
15203 assert!(
15205 src_delta.text.contains("point(at = [5mm, 6mm])"),
15206 "Expected new point in source, got: {}",
15207 src_delta.text
15208 );
15209 assert!(!scene_delta.new_objects.is_empty());
15210
15211 ctx.close().await;
15212 mock_ctx.close().await;
15213 }
15214
15215 #[tokio::test(flavor = "multi_thread")]
15216 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15217 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15218
15219 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15220 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15221
15222 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15223 assert!(
15224 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15225 "Expected experimental settings to be added to source"
15226 );
15227 }
15228
15229 #[tokio::test(flavor = "multi_thread")]
15230 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15231 let initial_source = "@settings(defaultLengthUnit = mm)
15233
15234s = sketch(on = XY) {}
15235";
15236
15237 let program = Program::parse(initial_source).unwrap().0.unwrap();
15238 let mut frontend = FrontendState::new();
15239
15240 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15241 let mock_ctx = ExecutorContext::new_mock(None).await;
15242 let version = Version(0);
15243
15244 frontend.hack_set_program(&ctx, program).await.unwrap();
15245 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15246 let sketch_id = sketch_object.id;
15247
15248 let line_ctor = LineCtor {
15249 start: Point2d {
15250 x: Expr::Number(Number {
15251 value: 0.0,
15252 units: NumericSuffix::Mm,
15253 }),
15254 y: Expr::Number(Number {
15255 value: 0.0,
15256 units: NumericSuffix::Mm,
15257 }),
15258 },
15259 end: Point2d {
15260 x: Expr::Number(Number {
15261 value: 10.0,
15262 units: NumericSuffix::Mm,
15263 }),
15264 y: Expr::Number(Number {
15265 value: 10.0,
15266 units: NumericSuffix::Mm,
15267 }),
15268 },
15269 construction: None,
15270 };
15271 let segment = SegmentCtor::Line(line_ctor);
15272 let (src_delta, scene_delta) = frontend
15273 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15274 .await
15275 .unwrap();
15276 assert!(
15277 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15278 "Expected line in source, got: {}",
15279 src_delta.text
15280 );
15281 assert_eq!(scene_delta.new_objects.len(), 3);
15283
15284 ctx.close().await;
15285 mock_ctx.close().await;
15286 }
15287
15288 #[tokio::test(flavor = "multi_thread")]
15289 async fn test_extra_newlines_between_operations_edit_line() {
15290 let initial_source = "@settings(defaultLengthUnit = mm)
15292
15293sketch001 = sketch(on = XY) {
15294
15295 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15296
15297}
15298";
15299
15300 let program = Program::parse(initial_source).unwrap().0.unwrap();
15301 let mut frontend = FrontendState::new();
15302
15303 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15304 let mock_ctx = ExecutorContext::new_mock(None).await;
15305 let version = Version(0);
15306 let project_id = ProjectId(0);
15307 let file_id = FileId(0);
15308
15309 frontend.hack_set_program(&ctx, program).await.unwrap();
15310 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15311 let sketch_id = sketch_object.id;
15312 let sketch = expect_sketch(sketch_object);
15313
15314 let line_id = sketch
15316 .segments
15317 .iter()
15318 .copied()
15319 .find(|seg_id| {
15320 matches!(
15321 &frontend.scene_graph.objects[seg_id.0].kind,
15322 ObjectKind::Segment {
15323 segment: Segment::Line(_)
15324 }
15325 )
15326 })
15327 .expect("Expected a line segment in sketch");
15328
15329 frontend
15331 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15332 .await
15333 .unwrap();
15334
15335 let line_ctor = LineCtor {
15337 start: Point2d {
15338 x: Expr::Var(Number {
15339 value: 1.0,
15340 units: NumericSuffix::Mm,
15341 }),
15342 y: Expr::Var(Number {
15343 value: 2.0,
15344 units: NumericSuffix::Mm,
15345 }),
15346 },
15347 end: Point2d {
15348 x: Expr::Var(Number {
15349 value: 13.0,
15350 units: NumericSuffix::Mm,
15351 }),
15352 y: Expr::Var(Number {
15353 value: 14.0,
15354 units: NumericSuffix::Mm,
15355 }),
15356 },
15357 construction: None,
15358 };
15359 let segments = vec![ExistingSegmentCtor {
15360 id: line_id,
15361 ctor: SegmentCtor::Line(line_ctor),
15362 }];
15363 let (src_delta, _scene_delta) = frontend
15364 .edit_segments(&mock_ctx, version, sketch_id, segments)
15365 .await
15366 .unwrap();
15367 assert!(
15368 src_delta
15369 .text
15370 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15371 "Expected edited line in source, got: {}",
15372 src_delta.text
15373 );
15374
15375 ctx.close().await;
15376 mock_ctx.close().await;
15377 }
15378
15379 #[tokio::test(flavor = "multi_thread")]
15380 async fn test_extra_newlines_delete_segment() {
15381 let initial_source = "@settings(defaultLengthUnit = mm)
15383
15384sketch001 = sketch(on = XY) {
15385 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15386}
15387";
15388
15389 let program = Program::parse(initial_source).unwrap().0.unwrap();
15390 let mut frontend = FrontendState::new();
15391
15392 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15393 let mock_ctx = ExecutorContext::new_mock(None).await;
15394 let version = Version(0);
15395
15396 frontend.hack_set_program(&ctx, program).await.unwrap();
15397 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15398 let sketch_id = sketch_object.id;
15399 let sketch = expect_sketch(sketch_object);
15400
15401 assert_eq!(sketch.segments.len(), 3);
15403 let circle_id = sketch.segments[2];
15404
15405 let (src_delta, scene_delta) = frontend
15407 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15408 .await
15409 .unwrap();
15410 assert!(
15411 src_delta.text.contains("sketch(on = XY) {"),
15412 "Expected sketch block in source, got: {}",
15413 src_delta.text
15414 );
15415 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15416 let new_sketch = expect_sketch(new_sketch_object);
15417 assert_eq!(new_sketch.segments.len(), 0);
15418
15419 ctx.close().await;
15420 mock_ctx.close().await;
15421 }
15422
15423 #[tokio::test(flavor = "multi_thread")]
15424 async fn test_unformatted_source_add_arc() {
15425 let initial_source = "@settings(defaultLengthUnit = mm)
15427
15428sketch001 = sketch(on = XY) {
15429}
15430";
15431
15432 let program = Program::parse(initial_source).unwrap().0.unwrap();
15433 let mut frontend = FrontendState::new();
15434
15435 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15436 let mock_ctx = ExecutorContext::new_mock(None).await;
15437 let version = Version(0);
15438
15439 frontend.hack_set_program(&ctx, program).await.unwrap();
15440 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15441 let sketch_id = sketch_object.id;
15442
15443 let arc_ctor = ArcCtor {
15444 start: Point2d {
15445 x: Expr::Var(Number {
15446 value: 5.0,
15447 units: NumericSuffix::Mm,
15448 }),
15449 y: Expr::Var(Number {
15450 value: 0.0,
15451 units: NumericSuffix::Mm,
15452 }),
15453 },
15454 end: Point2d {
15455 x: Expr::Var(Number {
15456 value: 0.0,
15457 units: NumericSuffix::Mm,
15458 }),
15459 y: Expr::Var(Number {
15460 value: 5.0,
15461 units: NumericSuffix::Mm,
15462 }),
15463 },
15464 center: Point2d {
15465 x: Expr::Var(Number {
15466 value: 0.0,
15467 units: NumericSuffix::Mm,
15468 }),
15469 y: Expr::Var(Number {
15470 value: 0.0,
15471 units: NumericSuffix::Mm,
15472 }),
15473 },
15474 direction: None,
15475 construction: None,
15476 };
15477 let segment = SegmentCtor::Arc(arc_ctor);
15478 let (src_delta, scene_delta) = frontend
15479 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15480 .await
15481 .unwrap();
15482 assert!(
15483 src_delta
15484 .text
15485 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15486 "Expected arc in source, got: {}",
15487 src_delta.text
15488 );
15489 assert!(!scene_delta.new_objects.is_empty());
15490
15491 ctx.close().await;
15492 mock_ctx.close().await;
15493 }
15494
15495 #[tokio::test(flavor = "multi_thread")]
15496 async fn test_arc_direction_flows_to_source() {
15497 let initial_source = "@settings(defaultLengthUnit = mm)
15498
15499sketch001 = sketch(on = XY) {
15500}
15501";
15502
15503 let program = Program::parse(initial_source).unwrap().0.unwrap();
15504 let mut frontend = FrontendState::new();
15505
15506 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15507 let mock_ctx = ExecutorContext::new_mock(None).await;
15508 let version = Version(0);
15509
15510 frontend.hack_set_program(&ctx, program).await.unwrap();
15511 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15512 let sketch_id = sketch_object.id;
15513
15514 let point = |x: f64, y: f64| Point2d {
15515 x: Expr::Var(Number {
15516 value: x,
15517 units: NumericSuffix::Mm,
15518 }),
15519 y: Expr::Var(Number {
15520 value: y,
15521 units: NumericSuffix::Mm,
15522 }),
15523 };
15524
15525 let arc_ctor = ArcCtor {
15527 start: point(5.0, 0.0),
15528 end: point(0.0, 5.0),
15529 center: point(0.0, 0.0),
15530 direction: Some(ArcDirection::Cw),
15531 construction: None,
15532 };
15533 let (src_delta, scene_delta) = frontend
15534 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15535 .await
15536 .unwrap();
15537 assert!(
15538 src_delta.text.contains("direction = CW"),
15539 "Expected direction = CW in source, got: {}",
15540 src_delta.text
15541 );
15542 let arc_id = *scene_delta.new_objects.last().unwrap();
15544
15545 let edited_ctor = ArcCtor {
15549 start: point(0.0, -5.0),
15550 end: point(0.0, 5.0),
15551 center: point(0.0, 0.0),
15552 direction: Some(ArcDirection::Cw),
15553 construction: None,
15554 };
15555 let (src_delta, _scene_delta) = frontend
15556 .edit_segments(
15557 &mock_ctx,
15558 version,
15559 sketch_id,
15560 vec![ExistingSegmentCtor {
15561 id: arc_id,
15562 ctor: SegmentCtor::Arc(edited_ctor),
15563 }],
15564 )
15565 .await
15566 .unwrap();
15567 assert!(
15568 src_delta.text.contains("start = [var 0mm, var -5mm]"),
15569 "Expected edited start point in source, got: {}",
15570 src_delta.text
15571 );
15572 assert!(
15573 src_delta.text.contains("direction = CW"),
15574 "Expected direction = CW to be preserved in source, got: {}",
15575 src_delta.text
15576 );
15577
15578 let edited_ctor = ArcCtor {
15581 start: point(0.0, -5.0),
15582 end: point(0.0, 5.0),
15583 center: point(0.0, 0.0),
15584 direction: Some(ArcDirection::Ccw),
15585 construction: None,
15586 };
15587 let (src_delta, _scene_delta) = frontend
15588 .edit_segments(
15589 &mock_ctx,
15590 version,
15591 sketch_id,
15592 vec![ExistingSegmentCtor {
15593 id: arc_id,
15594 ctor: SegmentCtor::Arc(edited_ctor),
15595 }],
15596 )
15597 .await
15598 .unwrap();
15599 assert!(
15600 !src_delta.text.contains("direction"),
15601 "Expected direction argument to be removed from source, got: {}",
15602 src_delta.text
15603 );
15604
15605 ctx.close().await;
15606 mock_ctx.close().await;
15607 }
15608
15609 #[tokio::test(flavor = "multi_thread")]
15610 async fn test_extra_newlines_add_circle() {
15611 let initial_source = "@settings(defaultLengthUnit = mm)
15613
15614sketch001 = sketch(on = XY) {
15615}
15616";
15617
15618 let program = Program::parse(initial_source).unwrap().0.unwrap();
15619 let mut frontend = FrontendState::new();
15620
15621 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15622 let mock_ctx = ExecutorContext::new_mock(None).await;
15623 let version = Version(0);
15624
15625 frontend.hack_set_program(&ctx, program).await.unwrap();
15626 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15627 let sketch_id = sketch_object.id;
15628
15629 let circle_ctor = CircleCtor {
15630 start: Point2d {
15631 x: Expr::Var(Number {
15632 value: 5.0,
15633 units: NumericSuffix::Mm,
15634 }),
15635 y: Expr::Var(Number {
15636 value: 0.0,
15637 units: NumericSuffix::Mm,
15638 }),
15639 },
15640 center: Point2d {
15641 x: Expr::Var(Number {
15642 value: 0.0,
15643 units: NumericSuffix::Mm,
15644 }),
15645 y: Expr::Var(Number {
15646 value: 0.0,
15647 units: NumericSuffix::Mm,
15648 }),
15649 },
15650 construction: None,
15651 };
15652 let segment = SegmentCtor::Circle(circle_ctor);
15653 let (src_delta, scene_delta) = frontend
15654 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15655 .await
15656 .unwrap();
15657 assert!(
15658 src_delta
15659 .text
15660 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15661 "Expected circle in source, got: {}",
15662 src_delta.text
15663 );
15664 assert!(!scene_delta.new_objects.is_empty());
15665
15666 ctx.close().await;
15667 mock_ctx.close().await;
15668 }
15669
15670 #[tokio::test(flavor = "multi_thread")]
15671 async fn test_extra_newlines_add_constraint() {
15672 let initial_source = "@settings(defaultLengthUnit = mm)
15674
15675sketch001 = sketch(on = XY) {
15676 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15677 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15678}
15679";
15680
15681 let program = Program::parse(initial_source).unwrap().0.unwrap();
15682 let mut frontend = FrontendState::new();
15683
15684 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15685 let mock_ctx = ExecutorContext::new_mock(None).await;
15686 let version = Version(0);
15687 let project_id = ProjectId(0);
15688 let file_id = FileId(0);
15689
15690 frontend.hack_set_program(&ctx, program).await.unwrap();
15691 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15692 let sketch_id = sketch_object.id;
15693 let sketch = expect_sketch(sketch_object);
15694
15695 let line_ids: Vec<ObjectId> = sketch
15697 .segments
15698 .iter()
15699 .copied()
15700 .filter(|seg_id| {
15701 matches!(
15702 &frontend.scene_graph.objects[seg_id.0].kind,
15703 ObjectKind::Segment {
15704 segment: Segment::Line(_)
15705 }
15706 )
15707 })
15708 .collect();
15709 assert_eq!(line_ids.len(), 2, "Expected two line segments");
15710
15711 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15712 let ObjectKind::Segment {
15713 segment: Segment::Line(line1_data),
15714 } = &line1.kind
15715 else {
15716 panic!("Expected line");
15717 };
15718 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15719 let ObjectKind::Segment {
15720 segment: Segment::Line(line2_data),
15721 } = &line2.kind
15722 else {
15723 panic!("Expected line");
15724 };
15725
15726 let constraint = Constraint::Coincident(Coincident {
15728 segments: vec![line1_data.end.into(), line2_data.start.into()],
15729 });
15730
15731 frontend
15733 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15734 .await
15735 .unwrap();
15736 let (src_delta, _scene_delta) = frontend
15737 .add_constraint(&mock_ctx, version, sketch_id, constraint)
15738 .await
15739 .unwrap();
15740 assert!(
15741 src_delta.text.contains("coincident("),
15742 "Expected coincident constraint in source, got: {}",
15743 src_delta.text
15744 );
15745
15746 ctx.close().await;
15747 mock_ctx.close().await;
15748 }
15749
15750 #[tokio::test(flavor = "multi_thread")]
15751 async fn test_extra_newlines_add_line_then_edit_line() {
15752 let initial_source = "@settings(defaultLengthUnit = mm)
15754
15755sketch001 = sketch(on = XY) {
15756}
15757";
15758
15759 let program = Program::parse(initial_source).unwrap().0.unwrap();
15760 let mut frontend = FrontendState::new();
15761
15762 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15763 let mock_ctx = ExecutorContext::new_mock(None).await;
15764 let version = Version(0);
15765
15766 frontend.hack_set_program(&ctx, program).await.unwrap();
15767 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15768 let sketch_id = sketch_object.id;
15769
15770 let line_ctor = LineCtor {
15772 start: Point2d {
15773 x: Expr::Number(Number {
15774 value: 0.0,
15775 units: NumericSuffix::Mm,
15776 }),
15777 y: Expr::Number(Number {
15778 value: 0.0,
15779 units: NumericSuffix::Mm,
15780 }),
15781 },
15782 end: Point2d {
15783 x: Expr::Number(Number {
15784 value: 10.0,
15785 units: NumericSuffix::Mm,
15786 }),
15787 y: Expr::Number(Number {
15788 value: 10.0,
15789 units: NumericSuffix::Mm,
15790 }),
15791 },
15792 construction: None,
15793 };
15794 let segment = SegmentCtor::Line(line_ctor);
15795 let (src_delta, scene_delta) = frontend
15796 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15797 .await
15798 .unwrap();
15799 assert!(
15800 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15801 "Expected line in source after add, got: {}",
15802 src_delta.text
15803 );
15804 let line_id = *scene_delta.new_objects.last().unwrap();
15806
15807 let line_ctor = LineCtor {
15809 start: Point2d {
15810 x: Expr::Number(Number {
15811 value: 1.0,
15812 units: NumericSuffix::Mm,
15813 }),
15814 y: Expr::Number(Number {
15815 value: 2.0,
15816 units: NumericSuffix::Mm,
15817 }),
15818 },
15819 end: Point2d {
15820 x: Expr::Number(Number {
15821 value: 13.0,
15822 units: NumericSuffix::Mm,
15823 }),
15824 y: Expr::Number(Number {
15825 value: 14.0,
15826 units: NumericSuffix::Mm,
15827 }),
15828 },
15829 construction: None,
15830 };
15831 let segments = vec![ExistingSegmentCtor {
15832 id: line_id,
15833 ctor: SegmentCtor::Line(line_ctor),
15834 }];
15835 let (src_delta, scene_delta) = frontend
15836 .edit_segments(&mock_ctx, version, sketch_id, segments)
15837 .await
15838 .unwrap();
15839 assert!(
15840 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15841 "Expected edited line in source, got: {}",
15842 src_delta.text
15843 );
15844 assert_eq!(scene_delta.new_objects, vec![]);
15845
15846 ctx.close().await;
15847 mock_ctx.close().await;
15848 }
15849
15850 #[test]
15851 fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15852 let code = "\
15861foo1 = 1
15862sk = sketch() {
15863 p = var 1.5
15864}
158657 + 8
15866";
15867 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15868 let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15869 panic!("expected an expression statement");
15870 };
15871 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15872 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15873 &mut ast,
15874 &source_ref,
15875 AstMutateCommand::AddVariableDeclaration {
15876 prefix: "foo".to_owned(),
15877 },
15878 )
15879 .unwrap();
15880 let AstMutateCommandReturn::Name(name) = cmd_return else {
15881 panic!("expected a generated name");
15882 };
15883 assert_eq!(name, "foo2");
15884 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15885 panic!("expected the expression statement to become a variable declaration");
15886 };
15887 assert_eq!(decl.name(), "foo2");
15888 }
15889
15890 fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15892 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15893 panic!("expected a variable declaration");
15894 };
15895 let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15896 panic!("expected a function expression");
15897 };
15898 &func.body
15899 }
15900
15901 fn then_block_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15904 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15905 panic!("expected a variable declaration");
15906 };
15907 let ast::Expr::IfExpression(if_expr) = &decl.declaration.init else {
15908 panic!("expected an if expression");
15909 };
15910 &if_expr.then_val
15911 }
15912
15913 #[test]
15914 fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15915 let code = "\
15920fn build() {
15921 thing1 = 1
15922 10 + 20
15923 return thing1
15924}
15925";
15926 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15927 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15928 panic!("expected an expression statement");
15929 };
15930 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15931 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15932 &mut ast,
15933 &source_ref,
15934 AstMutateCommand::AddVariableDeclaration {
15935 prefix: "thing".to_owned(),
15936 },
15937 )
15938 .unwrap();
15939 let AstMutateCommandReturn::Name(name) = cmd_return else {
15940 panic!("expected a generated name");
15941 };
15942 assert_eq!(name, "thing2");
15943 let body = &function_body_at(&ast, 0).body;
15944 assert_eq!(body.len(), 3);
15945 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
15946 panic!("expected the expression statement to become a variable declaration");
15947 };
15948 assert_eq!(decl.name(), "thing2");
15949 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15951 panic!("expected a variable declaration");
15952 };
15953 assert_eq!(first.name(), "thing1");
15954 assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
15955 }
15956
15957 #[test]
15958 fn test_delete_node_in_function_body_preserves_leading_comment() {
15959 let code = "\
15963fn build() {
15964 a = 1
15965 // keep me
15966 b = 2
15967 return a
15968}
15969";
15970 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15971 let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
15972 panic!("expected a variable declaration");
15973 };
15974 assert_eq!(b_decl.name(), "b");
15975 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
15976 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
15977 let body = &function_body_at(&ast, 0).body;
15978 assert_eq!(body.len(), 2, "expected b to be deleted");
15979 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15980 panic!("expected a variable declaration");
15981 };
15982 assert_eq!(first.name(), "a");
15983 let ast::BodyItem::ReturnStatement(_) = &body[1] else {
15984 panic!("expected the return statement to remain");
15985 };
15986 assert!(
15987 body[1].get_comments().iter().any(|c| c.contains("keep me")),
15988 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
15989 body[1].get_comments()
15990 );
15991 }
15992
15993 #[test]
15994 fn test_add_variable_declaration_in_function_body_ignores_parameters() {
15995 let code = "\
16003fn build(thing2) {
16004 thing1 = 1
16005 10 + 20
16006 return thing1 + thing2
16007}
16008";
16009 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16010 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16011 panic!("expected an expression statement");
16012 };
16013 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16014 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16015 &mut ast,
16016 &source_ref,
16017 AstMutateCommand::AddVariableDeclaration {
16018 prefix: "thing".to_owned(),
16019 },
16020 )
16021 .unwrap();
16022 let AstMutateCommandReturn::Name(name) = cmd_return else {
16023 panic!("expected a generated name");
16024 };
16025 assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16026 }
16027
16028 #[test]
16029 fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16030 let code = "\
16031x = 1
16032y = if x > 0 {
16033 q1 = 1
16034 foo(q1)
16035 q1
16036} else {
16037 2
16038}
16039";
16040 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16041 let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16042 panic!("expected an expression statement");
16043 };
16044 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16045 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16046 &mut ast,
16047 &source_ref,
16048 AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16049 )
16050 .unwrap();
16051 let AstMutateCommandReturn::Name(name) = cmd_return else {
16052 panic!("expected a generated name");
16053 };
16054 assert_eq!(name, "q2");
16055 let body = &then_block_at(&ast, 1).body;
16056 assert_eq!(body.len(), 3);
16057 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16058 panic!("expected the expression statement to become a variable declaration");
16059 };
16060 assert_eq!(decl.name(), "q2");
16061 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16063 panic!("expected a variable declaration");
16064 };
16065 assert_eq!(first.name(), "q1");
16066 assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16067 }
16068
16069 #[test]
16070 fn test_delete_node_in_if_branch_preserves_leading_comment() {
16071 let code = "\
16074y = if true {
16075 a = 1
16076 // keep me
16077 b = 2
16078 a + b
16079} else {
16080 2
16081}
16082";
16083 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16084 let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16085 panic!("expected a variable declaration");
16086 };
16087 assert_eq!(b_decl.name(), "b");
16088 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16089 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16090 let body = &then_block_at(&ast, 0).body;
16091 assert_eq!(body.len(), 2, "expected b to be deleted");
16092 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16093 panic!("expected a variable declaration");
16094 };
16095 assert_eq!(first.name(), "a");
16096 let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16097 panic!("expected the tail expression to remain");
16098 };
16099 assert!(
16100 body[1].get_comments().iter().any(|c| c.contains("keep me")),
16101 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16102 body[1].get_comments()
16103 );
16104 }
16105}