1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_api::UnitLength;
9use kcl_error::CompilationIssue;
10use kcl_error::SourceRange;
11use serde::Serialize;
12use uuid::Uuid;
13
14use crate::ExecOutcome;
15use crate::ExecutorContext;
16use crate::KclError;
17use crate::KclErrorWithOutputs;
18use crate::KclValueView;
19use crate::Program;
20use crate::SegmentDragAnchor;
21use crate::collections::AhashIndexSet;
22use crate::execution::Artifact;
23use crate::execution::ArtifactGraph;
24use crate::execution::ArtifactId;
25use crate::execution::CapSubType;
26use crate::execution::CodeRef;
27use crate::execution::MockConfig;
28use crate::execution::SKETCH_BLOCK_PARAM_ON;
29use crate::execution::annotations::WarningLevel;
30use crate::execution::cache::SketchModeState;
31use crate::execution::cache::clear_mem_cache;
32use crate::execution::cache::read_old_memory;
33use crate::execution::cache::write_old_memory;
34use crate::execution::types::adjust_length;
35use crate::fmt::format_number_literal;
36use crate::front::Angle;
37use crate::front::ArcCtor;
38use crate::front::ArcDirection;
39use crate::front::CircleCtor;
40use crate::front::ControlPointSplineCtor;
41use crate::front::Distance;
42use crate::front::EqualRadius;
43use crate::front::Error;
44use crate::front::ExecResult;
45use crate::front::FixedPoint;
46use crate::front::Freedom;
47use crate::front::LinesEqualLength;
48use crate::front::Midpoint;
49use crate::front::Object;
50use crate::front::Parallel;
51use crate::front::Perpendicular;
52use crate::front::PointCtor;
53use crate::front::Symmetric;
54use crate::front::Tangent;
55use crate::frontend::api::CapSource;
56use crate::frontend::api::Expr;
57use crate::frontend::api::FileId;
58use crate::frontend::api::Number;
59use crate::frontend::api::ObjectId;
60use crate::frontend::api::ObjectKind;
61use crate::frontend::api::Plane;
62use crate::frontend::api::ProjectId;
63use crate::frontend::api::RestoreSketchCheckpointOutcome;
64use crate::frontend::api::SceneGraph;
65use crate::frontend::api::SceneGraphDelta;
66use crate::frontend::api::SketchCheckpointId;
67use crate::frontend::api::SourceDelta;
68use crate::frontend::api::SourceRef;
69use crate::frontend::api::SourceRefRange;
70use crate::frontend::api::Version;
71use crate::frontend::api::WallSource;
72use crate::frontend::modify::find_defined_names;
73use crate::frontend::modify::next_free_name;
74use crate::frontend::modify::next_free_name_with_padding;
75use crate::frontend::sketch::Coincident;
76use crate::frontend::sketch::Constraint;
77use crate::frontend::sketch::ConstraintLabelPositionEdit;
78use crate::frontend::sketch::ConstraintSegment;
79use crate::frontend::sketch::Diameter;
80use crate::frontend::sketch::ExistingSegmentCtor;
81use crate::frontend::sketch::Horizontal;
82use crate::frontend::sketch::LineCtor;
83use crate::frontend::sketch::Point2d;
84use crate::frontend::sketch::Radius;
85use crate::frontend::sketch::Segment;
86use crate::frontend::sketch::SegmentCtor;
87use crate::frontend::sketch::SketchApi;
88use crate::frontend::sketch::SketchCtor;
89use crate::frontend::sketch::Vertical;
90use crate::id::IncIdGenerator;
91use crate::parsing::ast::types as ast;
92use crate::parsing::ast::types::BoxNode;
93use crate::parsing::ast::types::CallExpressionKw;
94use crate::parsing::ast::types::NodePathExt;
95use crate::pretty::NumericSuffix;
96use crate::std::constraints::LinesAtAngleKind;
97use crate::walk::NodeMut;
98use crate::walk::Visitable;
99use crate::walk::traverse::MutateBodyItem;
100use crate::walk::traverse::TraversalReturn;
101use crate::walk::traverse::Visitor;
102use crate::walk::traverse::dfs_mut;
103
104pub(crate) mod api;
105pub(crate) mod modify;
106pub(crate) mod sketch;
107
108pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
109
110#[derive(Debug, Clone)]
111struct SketchCheckpoint {
112 id: SketchCheckpointId,
113 source: SourceDelta,
114 program: Program,
115 scene_graph: SceneGraph,
116 exec_outcome: ExecOutcome,
117 point_freedom_cache: HashMap<ObjectId, Freedom>,
118 mock_memory: Option<SketchModeState>,
119}
120pub(crate) mod trim;
121
122struct ArcSizeConstraintParams {
123 points: Vec<ObjectId>,
124 function_name: &'static str,
125 value: f64,
126 units: NumericSuffix,
127 label_position: Option<Point2d<Number>>,
128 constraint_type_name: &'static str,
129}
130
131const POINT_FN: &str = "point";
132const POINT_AT_PARAM: &str = "at";
133const LINE_FN: &str = "line";
134const LINE_VARIABLE: &str = "line";
135const LINE_START_PARAM: &str = "start";
136const LINE_END_PARAM: &str = "end";
137const ARC_FN: &str = "arc";
138const ARC_VARIABLE: &str = "arc";
139const ARC_START_PARAM: &str = "start";
140const ARC_END_PARAM: &str = "end";
141const ARC_CENTER_PARAM: &str = "center";
142const ARC_DIRECTION_PARAM: &str = "direction";
143const ARC_DIRECTION_CW_NAME: &str = "CW";
145const CIRCLE_FN: &str = "circle";
146const CIRCLE_VARIABLE: &str = "circle";
147const CIRCLE_START_PARAM: &str = "start";
148const CIRCLE_CENTER_PARAM: &str = "center";
149const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
150const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
151const LABEL_POSITION_PARAM: &str = "labelPosition";
152
153const COINCIDENT_FN: &str = "coincident";
154const DIAMETER_FN: &str = "diameter";
155const DISTANCE_FN: &str = "distance";
156const FIXED_FN: &str = "fixed";
157const ANGLE_FN: &str = "angle";
158const ANGLE_DIMENSION_FN: &str = "angleDimension";
159const ANGLE_LINES_PARAM: &str = "lines";
160const ANGLE_SECTOR_PARAM: &str = "sector";
161const ANGLE_INVERSE_PARAM: &str = "inverse";
162const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
163const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
164const EQUAL_LENGTH_FN: &str = "equalLength";
165const EQUAL_RADIUS_FN: &str = "equalRadius";
166const HORIZONTAL_FN: &str = "horizontal";
167const MIDPOINT_FN: &str = "midpoint";
168const MIDPOINT_POINT_PARAM: &str = "point";
169const RADIUS_FN: &str = "radius";
170const SYMMETRIC_FN: &str = "symmetric";
171const SYMMETRIC_AXIS_PARAM: &str = "axis";
172const TANGENT_FN: &str = "tangent";
173const VERTICAL_FN: &str = "vertical";
174
175const LINE_PROPERTY_START: &str = "start";
176const LINE_PROPERTY_END: &str = "end";
177
178const ARC_PROPERTY_START: &str = "start";
179const ARC_PROPERTY_END: &str = "end";
180const ARC_PROPERTY_CENTER: &str = "center";
181const CIRCLE_PROPERTY_START: &str = "start";
182const CIRCLE_PROPERTY_CENTER: &str = "center";
183const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
184const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
185
186const CONSTRUCTION_PARAM: &str = "construction";
187
188#[derive(Debug, Clone, Copy)]
189enum EditDeleteKind {
190 Edit,
191 DeleteNonSketch,
192}
193
194struct ExecuteAfterEditOptions {
196 segment_ids_edited: AhashIndexSet<ObjectId>,
197 edit_kind: EditDeleteKind,
198 commit_solved_initial_guesses: bool,
199}
200
201impl EditDeleteKind {
202 fn is_delete(&self) -> bool {
204 match self {
205 EditDeleteKind::Edit => false,
206 EditDeleteKind::DeleteNonSketch => true,
207 }
208 }
209
210 fn to_change_kind(self) -> ChangeKind {
211 match self {
212 EditDeleteKind::Edit => ChangeKind::Edit,
213 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
214 }
215 }
216}
217
218#[derive(Debug, Clone, Copy)]
219enum ChangeKind {
220 Add,
221 Edit,
222 Delete,
223 None,
224}
225
226#[derive(Debug, Clone, Serialize, ts_rs::TS)]
227#[ts(export, export_to = "FrontendApi.ts")]
228#[serde(tag = "type")]
229pub enum SetProgramOutcome {
230 #[serde(rename_all = "camelCase")]
231 Success {
232 scene_graph: Box<SceneGraph>,
233 exec_outcome: Box<ExecOutcome>,
234 checkpoint_id: Option<SketchCheckpointId>,
235 },
236 #[serde(rename_all = "camelCase")]
237 ExecFailure { error: Box<KclErrorWithOutputs> },
238}
239
240pub struct EditSegmentsOptions {
242 pub anchor_segment_ids: Option<Vec<ObjectId>>,
248 pub drag_anchors: Vec<SegmentDragAnchor>,
251 pub constraint_label_edits: Vec<ConstraintLabelPositionEdit>,
254 pub commit_solved_initial_guesses: bool,
256}
257
258pub struct EditDistanceConstraintLabelPositionOptions {
260 pub anchor_segment_ids: Vec<ObjectId>,
262 pub commit_solved_initial_guesses: bool,
264}
265
266pub struct EditConstraintOptions {
268 pub commit_solved_initial_guesses: bool,
270}
271
272#[derive(Debug, Clone)]
273struct SolidAstReference {
274 variable_name: String,
275 output_index: Option<usize>,
276}
277
278#[derive(Debug, Clone)]
279pub struct FrontendState {
280 program: Program,
281 scene_graph: SceneGraph,
282 solid_references: HashMap<Uuid, SolidAstReference>,
284 point_freedom_cache: HashMap<ObjectId, Freedom>,
287 next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
290 next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
293 next_constraint_label_edits: Option<Vec<ConstraintLabelPositionEdit>>,
295 next_edit_commits_solver_solutions: Option<bool>,
299 sketch_checkpoints: VecDeque<SketchCheckpoint>,
300 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
301}
302
303impl Default for FrontendState {
304 fn default() -> Self {
305 Self::new()
306 }
307}
308
309impl FrontendState {
310 pub fn new() -> Self {
311 Self {
312 program: Program::empty(),
313 scene_graph: SceneGraph {
314 project: ProjectId(0),
315 file: FileId(0),
316 version: Version(0),
317 objects: Default::default(),
318 settings: Default::default(),
319 sketch_mode: Default::default(),
320 },
321 solid_references: HashMap::new(),
322 point_freedom_cache: HashMap::new(),
323 next_drag_anchor_segment_ids: None,
324 next_segment_drag_anchors: None,
325 next_constraint_label_edits: None,
326 next_edit_commits_solver_solutions: None,
327 sketch_checkpoints: VecDeque::new(),
328 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
329 }
330 }
331
332 pub fn scene_graph(&self) -> &SceneGraph {
334 &self.scene_graph
335 }
336
337 pub fn default_length_unit(&self) -> UnitLength {
338 self.program
339 .meta_settings()
340 .ok()
341 .flatten()
342 .map(|settings| settings.default_length_units)
343 .unwrap_or(UnitLength::Millimeters)
344 }
345
346 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
347 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
348
349 let checkpoint = SketchCheckpoint {
350 id: checkpoint_id,
351 source: SourceDelta {
352 text: source_from_ast(&self.program.ast),
353 },
354 program: self.program.clone(),
355 scene_graph: self.scene_graph.clone(),
356 exec_outcome,
357 point_freedom_cache: self.point_freedom_cache.clone(),
358 mock_memory: read_old_memory().await,
359 };
360
361 self.sketch_checkpoints.push_back(checkpoint);
362 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
363 self.sketch_checkpoints.pop_front();
364 }
365
366 Ok(checkpoint_id)
367 }
368
369 pub async fn edit_segments_with_options(
377 &mut self,
378 ctx: &ExecutorContext,
379 version: Version,
380 sketch: ObjectId,
381 segments: Vec<ExistingSegmentCtor>,
382 options: EditSegmentsOptions,
383 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
384 let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
385 self.next_drag_anchor_segment_ids
386 .replace(anchor_ids.into_iter().collect())
387 });
388 let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
389 let previous_constraint_label_edits = self.next_constraint_label_edits.replace(options.constraint_label_edits);
390 let previous_commit_mode = self
391 .next_edit_commits_solver_solutions
392 .replace(options.commit_solved_initial_guesses);
393 let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
394 if let Some(previous_anchor_ids) = previous_anchor_ids {
395 self.next_drag_anchor_segment_ids = previous_anchor_ids;
396 }
397 self.next_segment_drag_anchors = previous_drag_anchors;
398 self.next_constraint_label_edits = previous_constraint_label_edits;
399 self.next_edit_commits_solver_solutions = previous_commit_mode;
400 result
401 }
402
403 pub async fn edit_distance_constraint_label_position_with_options(
409 &mut self,
410 ctx: &ExecutorContext,
411 version: Version,
412 sketch: ObjectId,
413 constraint_id: ObjectId,
414 label_position: Point2d<Number>,
415 options: EditDistanceConstraintLabelPositionOptions,
416 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
417 let previous_commit_mode = self
418 .next_edit_commits_solver_solutions
419 .replace(options.commit_solved_initial_guesses);
420 let result = SketchApi::edit_distance_constraint_label_position(
421 self,
422 ctx,
423 version,
424 sketch,
425 constraint_id,
426 label_position,
427 options.anchor_segment_ids,
428 )
429 .await;
430 self.next_edit_commits_solver_solutions = previous_commit_mode;
431 result
432 }
433
434 pub async fn edit_distance_constraint_with_options(
436 &mut self,
437 ctx: &ExecutorContext,
438 version: Version,
439 sketch: ObjectId,
440 constraint_id: ObjectId,
441 constraint: Constraint,
442 options: EditConstraintOptions,
443 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
444 self.edit_constraint_with_options(ctx, version, sketch, constraint_id, constraint, options)
445 .await
446 }
447
448 pub async fn edit_angle_constraint_with_options(
450 &mut self,
451 ctx: &ExecutorContext,
452 version: Version,
453 sketch: ObjectId,
454 constraint_id: ObjectId,
455 angle: Angle,
456 options: EditConstraintOptions,
457 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
458 self.edit_constraint_with_options(ctx, version, sketch, constraint_id, Constraint::Angle(angle), options)
459 .await
460 }
461
462 pub async fn edit_constraint_with_options(
464 &mut self,
465 ctx: &ExecutorContext,
466 _version: Version,
467 sketch: ObjectId,
468 constraint_id: ObjectId,
469 constraint: Constraint,
470 options: EditConstraintOptions,
471 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
472 let sketch_block_ref =
474 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
475
476 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
477 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
478 })?;
479
480 let mut new_ast = self.program.ast.clone();
481 let command = match &object.kind {
482 ObjectKind::Constraint {
483 constraint:
484 Constraint::Distance(_) | Constraint::HorizontalDistance(_) | Constraint::VerticalDistance(_),
485 } => {
486 let (function_name, distance) = match &constraint {
487 Constraint::Distance(distance) => (DISTANCE_FN, distance),
488 Constraint::HorizontalDistance(distance) => (HORIZONTAL_DISTANCE_FN, distance),
489 Constraint::VerticalDistance(distance) => (VERTICAL_DISTANCE_FN, distance),
490 _ => {
491 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
492 "A distance constraint can only be replaced by another distance constraint".to_owned(),
493 )));
494 }
495 };
496 let (call, value) = self
497 .distance_constraint_ast_parts(function_name, distance, &mut new_ast)
498 .map_err(KclErrorWithOutputs::no_outputs)?;
499 AstMutateCommand::EditDistanceConstraint { call, value }
500 }
501 ObjectKind::Constraint {
502 constraint: Constraint::Angle(_),
503 } => {
504 let Constraint::Angle(angle) = &constraint else {
505 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
506 "An angle constraint can only be replaced by another angle constraint".to_owned(),
507 )));
508 };
509 let (call, value) = self
510 .angle_constraint_ast_parts(angle, &mut new_ast)
511 .map_err(KclErrorWithOutputs::no_outputs)?;
512 AstMutateCommand::EditAngleConstraint { call, value }
513 }
514 ObjectKind::Constraint { .. } => {
515 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
516 "Editing {} is not supported",
517 object.kind.human_friendly_kind_with_article(),
518 ))));
519 }
520 _ => {
521 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
522 "Object is not a constraint: {constraint_id:?}"
523 ))));
524 }
525 };
526
527 self.mutate_ast(&mut new_ast, constraint_id, command)
528 .map_err(KclErrorWithOutputs::no_outputs)?;
529
530 self.execute_after_edit(
531 ctx,
532 sketch,
533 sketch_block_ref,
534 &mut new_ast,
535 ExecuteAfterEditOptions {
536 segment_ids_edited: Default::default(),
537 edit_kind: EditDeleteKind::Edit,
538 commit_solved_initial_guesses: options.commit_solved_initial_guesses,
539 },
540 )
541 .await
542 }
543
544 pub async fn restore_sketch_checkpoint(
545 &mut self,
546 checkpoint_id: SketchCheckpointId,
547 ) -> api::Result<RestoreSketchCheckpointOutcome> {
548 let checkpoint = self
549 .sketch_checkpoints
550 .iter()
551 .find(|checkpoint| checkpoint.id == checkpoint_id)
552 .cloned()
553 .ok_or_else(|| Error {
554 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
555 })?;
556
557 self.program = checkpoint.program;
558 self.scene_graph = checkpoint.scene_graph.clone();
559 self.solid_references = solid_references_from_variables(&self.program.ast, &checkpoint.exec_outcome.variables);
560 self.point_freedom_cache = checkpoint.point_freedom_cache;
561 self.next_drag_anchor_segment_ids = None;
562 self.next_segment_drag_anchors = None;
563 self.next_constraint_label_edits = None;
564 self.next_edit_commits_solver_solutions = None;
565
566 if let Some(mock_memory) = checkpoint.mock_memory {
567 write_old_memory(mock_memory).await;
568 } else {
569 clear_mem_cache().await;
570 }
571
572 Ok(RestoreSketchCheckpointOutcome {
573 source_delta: checkpoint.source,
574 scene_graph_delta: SceneGraphDelta {
575 new_graph: self.scene_graph_for_ui(),
576 new_objects: Vec::new(),
577 invalidates_ids: true,
578 exec_outcome: checkpoint.exec_outcome,
579 },
580 })
581 }
582
583 pub fn clear_sketch_checkpoints(&mut self) {
584 self.sketch_checkpoints.clear();
585 }
586 fn scene_graph_for_ui(&self) -> SceneGraph {
587 let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
588 matches!(
589 object.kind,
590 ObjectKind::Segment {
591 segment: Segment::ControlPointSpline(_)
592 }
593 )
594 });
595
596 if !has_control_point_splines {
597 return self.scene_graph.clone();
598 }
599
600 let hidden_constraint_ids = self
601 .scene_graph
602 .objects
603 .iter()
604 .filter_map(|object| match &object.kind {
605 ObjectKind::Constraint {
606 constraint: Constraint::Coincident(coincident),
607 } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
608 Some(object.id)
609 }
610 _ => None,
611 })
612 .collect::<HashSet<_>>();
613
614 if hidden_constraint_ids.is_empty() {
615 return self.scene_graph.clone();
616 }
617
618 let mut scene_graph = self.scene_graph.clone();
619 for object in &mut scene_graph.objects {
620 match &mut object.kind {
621 ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
622 object.kind = ObjectKind::Nil;
623 }
624 ObjectKind::Sketch(sketch) => {
625 sketch
626 .constraints
627 .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
628 }
629 _ => {}
630 }
631 }
632
633 scene_graph
634 }
635}
636
637fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
638 let mut first_owner_id = None;
639 for segment_id in coincident.segment_ids() {
640 let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
641 return false;
642 };
643
644 match first_owner_id {
645 Some(first_owner_id) if first_owner_id != owner_id => return false,
646 Some(_) => {}
647 None => first_owner_id = Some(owner_id),
648 }
649 }
650
651 first_owner_id.is_some()
652}
653
654fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
655 let object = scene_graph.objects.get(segment_id.0)?;
656 let ObjectKind::Segment { segment } = &object.kind else {
657 return None;
658 };
659
660 match segment {
661 Segment::ControlPointSpline(_) => Some(segment_id),
662 Segment::Point(point) => point
663 .owner
664 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
665 Segment::Line(line) => line
666 .owner
667 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
668 _ => None,
669 }
670}
671
672fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
673 matches!(
674 scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
675 Some(ObjectKind::Segment {
676 segment: Segment::ControlPointSpline(_)
677 })
678 )
679}
680
681fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
682 let experimental_features_allowed = program
683 .meta_settings()
684 .ok()
685 .flatten()
686 .map(|settings| settings.experimental_features == WarningLevel::Allow)
687 .unwrap_or(false);
688 if experimental_features_allowed {
689 return Ok(program.clone());
690 }
691
692 program.change_experimental_features(Some(WarningLevel::Allow))
693}
694
695impl SketchApi for FrontendState {
696 async fn execute_mock(
697 &mut self,
698 ctx: &ExecutorContext,
699 _version: Version,
700 sketch: ObjectId,
701 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
702 let sketch_block_ref =
703 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
704
705 let mut truncated_program = self.program.clone();
706 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
707 .map_err(KclErrorWithOutputs::no_outputs)?;
708
709 let outcome = ctx
711 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
712 .await?;
713 let new_source = source_from_ast(&self.program.ast);
714 let src_delta = SourceDelta { text: new_source };
715 let outcome = self.update_state_after_exec(outcome, true);
717 let scene_graph_delta = SceneGraphDelta {
718 new_graph: self.scene_graph.clone(),
719 new_objects: Default::default(),
720 invalidates_ids: false,
721 exec_outcome: outcome,
722 };
723 Ok((src_delta, scene_graph_delta))
724 }
725
726 async fn new_sketch(
727 &mut self,
728 ctx: &ExecutorContext,
729 _project: ProjectId,
730 _file: FileId,
731 _version: Version,
732 args: SketchCtor,
733 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
734 let mut new_ast = self.program.ast.clone();
737 let mut plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &self.solid_references, &args.on)
739 .map_err(KclErrorWithOutputs::no_outputs)?;
740 let mut defined_names = find_defined_names(&new_ast);
741 let is_face_of_expr = matches!(
742 &plane_ast,
743 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
744 );
745 if is_face_of_expr {
746 let face_name = next_free_name_with_padding("face", &defined_names)
747 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
748 let face_decl = ast::VariableDeclaration::new(
749 ast::VariableDeclarator::new(&face_name, plane_ast),
750 ast::ItemVisibility::Default,
751 ast::VariableKind::Const,
752 );
753 new_ast
754 .body
755 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
756 face_decl,
757 ))));
758 defined_names.insert(face_name.clone());
759 plane_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(&face_name)));
760 }
761 let sketch_ast = ast::SketchBlock {
762 arguments: vec![ast::LabeledArg {
763 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
764 arg: plane_ast,
765 }],
766 body: Default::default(),
767 is_being_edited: false,
768 non_code_meta: Default::default(),
769 digest: None,
770 };
771 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
774 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
775 let sketch_decl = ast::VariableDeclaration::new(
776 ast::VariableDeclarator::new(
777 &sketch_name,
778 ast::Expr::SketchBlock(BoxNode::new(ast::Node::no_src(sketch_ast))),
779 ),
780 ast::ItemVisibility::Default,
781 ast::VariableKind::Const,
782 );
783 new_ast
784 .body
785 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
786 sketch_decl,
787 ))));
788 let new_source = source_from_ast(&new_ast);
790 let new_program = parse_frontend_mutation_source(
792 &new_source,
793 "Error parsing KCL source after adding sketch",
794 "No AST produced after adding sketch",
795 )?;
796
797 self.program = new_program.clone();
799
800 let outcome = ctx.run_with_caching(new_program.clone()).await?;
803 let freedom_analysis_ran = true;
804
805 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
806
807 let Some(sketch_id) = self
808 .scene_graph
809 .objects
810 .iter()
811 .filter_map(|object| match object.kind {
812 ObjectKind::Sketch(_) => Some(object.id),
813 _ => None,
814 })
815 .max_by_key(|id| id.0)
816 else {
817 return Err(KclErrorWithOutputs::from_error_outcome(
818 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
819 outcome,
820 ));
821 };
822 self.scene_graph.sketch_mode = Some(sketch_id);
824
825 let src_delta = SourceDelta { text: new_source };
826 let scene_graph_delta = SceneGraphDelta {
827 new_graph: self.scene_graph_for_ui(),
828 invalidates_ids: false,
829 new_objects: vec![sketch_id],
830 exec_outcome: outcome,
831 };
832 Ok((src_delta, scene_graph_delta, sketch_id))
833 }
834
835 async fn edit_sketch(
836 &mut self,
837 ctx: &ExecutorContext,
838 _project: ProjectId,
839 _file: FileId,
840 _version: Version,
841 sketch: ObjectId,
842 ) -> ExecResult<SceneGraphDelta> {
843 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
847 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
848 })?;
849 let ObjectKind::Sketch(_) = &sketch_object.kind else {
850 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
851 "Object is not a sketch, it is {}",
852 sketch_object.kind.human_friendly_kind_with_article()
853 ))));
854 };
855 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
856
857 self.scene_graph.sketch_mode = Some(sketch);
859
860 let mut truncated_program = self.program.clone();
862 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
863 .map_err(KclErrorWithOutputs::no_outputs)?;
864
865 let outcome = ctx
868 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
869 .await?;
870
871 let outcome = self.update_state_after_exec(outcome, true);
873 let scene_graph_delta = SceneGraphDelta {
874 new_graph: self.scene_graph_for_ui(),
875 invalidates_ids: false,
876 new_objects: Vec::new(),
877 exec_outcome: outcome,
878 };
879 Ok(scene_graph_delta)
880 }
881
882 async fn exit_sketch(
883 &mut self,
884 ctx: &ExecutorContext,
885 _version: Version,
886 sketch: ObjectId,
887 ) -> ExecResult<SceneGraph> {
888 #[cfg(not(target_arch = "wasm32"))]
890 let _ = sketch;
891 #[cfg(target_arch = "wasm32")]
892 if self.scene_graph.sketch_mode != Some(sketch) {
893 web_sys::console::warn_1(
894 &format!(
895 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
896 self.scene_graph.sketch_mode
897 )
898 .into(),
899 );
900 }
901 self.scene_graph.sketch_mode = None;
902
903 let outcome = ctx.run_with_caching(self.program.clone()).await?;
905
906 self.update_state_after_exec(outcome, false);
908
909 Ok(self.scene_graph_for_ui())
910 }
911
912 async fn delete_sketch(
913 &mut self,
914 ctx: &ExecutorContext,
915 _version: Version,
916 sketch: ObjectId,
917 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
918 let mut new_ast = self.program.ast.clone();
921
922 let sketch_id = sketch;
924 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
925 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
926 })?;
927 let ObjectKind::Sketch(_) = &sketch_object.kind else {
928 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
929 "Object is not a sketch, it is {}",
930 sketch_object.kind.human_friendly_kind_with_article(),
931 ))));
932 };
933
934 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
936 .map_err(KclErrorWithOutputs::no_outputs)?;
937
938 self.execute_after_delete_sketch(ctx, &mut new_ast).await
939 }
940
941 async fn add_segment(
942 &mut self,
943 ctx: &ExecutorContext,
944 _version: Version,
945 sketch: ObjectId,
946 segment: SegmentCtor,
947 _label: Option<String>,
948 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
949 match segment {
951 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
952 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
953 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
954 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
955 SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
956 }
957 }
958
959 async fn edit_segments(
960 &mut self,
961 ctx: &ExecutorContext,
962 _version: Version,
963 sketch: ObjectId,
964 segments: Vec<ExistingSegmentCtor>,
965 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
966 let sketch_block_ref =
968 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
969
970 let mut new_ast = self.program.ast.clone();
971 let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
972 let mut invalidates_ids = false;
973
974 for segment in &segments {
977 edited_segment_ids.insert(segment.id);
978 if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
979 && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
980 && let ObjectKind::Segment {
981 segment: Segment::ControlPointSpline(existing_spline),
982 } = &existing_object.kind
983 && existing_spline.controls.len() != new_ctor.points.len()
984 {
985 invalidates_ids = true;
986 }
987 }
988 let drag_anchor_segment_ids = self
989 .next_drag_anchor_segment_ids
990 .take()
991 .unwrap_or_else(|| edited_segment_ids.clone());
992 let constraint_label_edits = self.next_constraint_label_edits.take().unwrap_or_default();
993 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
994
995 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
1010
1011 for segment in segments {
1012 let segment_id = segment.id;
1013 match segment.ctor {
1014 SegmentCtor::Point(ctor) => {
1015 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
1017 && let ObjectKind::Segment { segment } = &segment_object.kind
1018 && let Segment::Point(point) = segment
1019 && let Some(owner_id) = point.owner
1020 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1021 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1022 {
1023 match owner_segment {
1024 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
1025 if let Some(existing) = final_edits.get_mut(&owner_id) {
1026 let SegmentCtor::Line(line_ctor) = existing else {
1027 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1028 "Internal: Expected line ctor for owner, but found {}",
1029 existing.human_friendly_kind_with_article()
1030 ))));
1031 };
1032 if line.start == segment_id {
1034 line_ctor.start = ctor.position;
1035 } else {
1036 line_ctor.end = ctor.position;
1037 }
1038 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
1039 let mut line_ctor = line_ctor.clone();
1041 if line.start == segment_id {
1042 line_ctor.start = ctor.position;
1043 } else {
1044 line_ctor.end = ctor.position;
1045 }
1046 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
1047 } else {
1048 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1050 "Internal: Line does not have line ctor, but found {}",
1051 line.ctor.human_friendly_kind_with_article()
1052 ))));
1053 }
1054 continue;
1055 }
1056 Segment::Arc(arc)
1057 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
1058 {
1059 if let Some(existing) = final_edits.get_mut(&owner_id) {
1060 let SegmentCtor::Arc(arc_ctor) = existing else {
1061 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1062 "Internal: Expected arc ctor for owner, but found {}",
1063 existing.human_friendly_kind_with_article()
1064 ))));
1065 };
1066 if arc.start == segment_id {
1067 arc_ctor.start = ctor.position;
1068 } else if arc.end == segment_id {
1069 arc_ctor.end = ctor.position;
1070 } else {
1071 arc_ctor.center = ctor.position;
1072 }
1073 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
1074 let mut arc_ctor = arc_ctor.clone();
1075 if arc.start == segment_id {
1076 arc_ctor.start = ctor.position;
1077 } else if arc.end == segment_id {
1078 arc_ctor.end = ctor.position;
1079 } else {
1080 arc_ctor.center = ctor.position;
1081 }
1082 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
1083 } else {
1084 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1085 "Internal: Arc does not have arc ctor, but found {}",
1086 arc.ctor.human_friendly_kind_with_article()
1087 ))));
1088 }
1089 continue;
1090 }
1091 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
1092 if let Some(existing) = final_edits.get_mut(&owner_id) {
1093 let SegmentCtor::Circle(circle_ctor) = existing else {
1094 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1095 "Internal: Expected circle ctor for owner, but found {}",
1096 existing.human_friendly_kind_with_article()
1097 ))));
1098 };
1099 if circle.start == segment_id {
1100 circle_ctor.start = ctor.position;
1101 } else {
1102 circle_ctor.center = ctor.position;
1103 }
1104 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
1105 let mut circle_ctor = circle_ctor.clone();
1106 if circle.start == segment_id {
1107 circle_ctor.start = ctor.position;
1108 } else {
1109 circle_ctor.center = ctor.position;
1110 }
1111 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
1112 } else {
1113 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1114 "Internal: Circle does not have circle ctor, but found {}",
1115 circle.ctor.human_friendly_kind_with_article()
1116 ))));
1117 }
1118 continue;
1119 }
1120 Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
1121 let Some(control_index) =
1122 spline.controls.iter().position(|control_id| *control_id == segment_id)
1123 else {
1124 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1125 "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
1126 ))));
1127 };
1128 if let Some(existing) = final_edits.get_mut(&owner_id) {
1129 let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
1130 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1131 "Internal: Expected controlPointSpline ctor for owner, but found {}",
1132 existing.human_friendly_kind_with_article()
1133 ))));
1134 };
1135 spline_ctor.points[control_index] = ctor.position;
1136 } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
1137 let mut spline_ctor = spline_ctor.clone();
1138 spline_ctor.points[control_index] = ctor.position;
1139 final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
1140 } else {
1141 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1142 "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1143 spline.ctor.human_friendly_kind_with_article()
1144 ))));
1145 }
1146 continue;
1147 }
1148 _ => {}
1149 }
1150 }
1151
1152 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1154 }
1155 SegmentCtor::Line(ctor) => {
1156 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1157 }
1158 SegmentCtor::Arc(ctor) => {
1159 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1160 }
1161 SegmentCtor::Circle(ctor) => {
1162 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1163 }
1164 SegmentCtor::ControlPointSpline(ctor) => {
1165 final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1166 }
1167 }
1168 }
1169
1170 for (segment_id, ctor) in final_edits {
1171 match ctor {
1172 SegmentCtor::Point(ctor) => self
1173 .edit_point(&mut new_ast, sketch, segment_id, ctor)
1174 .map_err(KclErrorWithOutputs::no_outputs)?,
1175 SegmentCtor::Line(ctor) => self
1176 .edit_line(&mut new_ast, sketch, segment_id, ctor)
1177 .map_err(KclErrorWithOutputs::no_outputs)?,
1178 SegmentCtor::Arc(ctor) => self
1179 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1180 .map_err(KclErrorWithOutputs::no_outputs)?,
1181 SegmentCtor::Circle(ctor) => self
1182 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1183 .map_err(KclErrorWithOutputs::no_outputs)?,
1184 SegmentCtor::ControlPointSpline(ctor) => self
1185 .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1186 .map_err(KclErrorWithOutputs::no_outputs)?,
1187 }
1188 }
1189 for edit in constraint_label_edits {
1190 self.mutate_constraint_label_position(&mut new_ast, edit.constraint_id, edit.label_position)
1191 .map_err(KclErrorWithOutputs::no_outputs)?;
1192 }
1193 let (source_delta, mut scene_graph_delta) = self
1194 .execute_after_edit(
1195 ctx,
1196 sketch,
1197 sketch_block_ref,
1198 &mut new_ast,
1199 ExecuteAfterEditOptions {
1200 segment_ids_edited: drag_anchor_segment_ids,
1201 edit_kind: EditDeleteKind::Edit,
1202 commit_solved_initial_guesses,
1203 },
1204 )
1205 .await?;
1206 if invalidates_ids {
1207 scene_graph_delta.invalidates_ids = true;
1208 }
1209 Ok((source_delta, scene_graph_delta))
1210 }
1211
1212 async fn delete_objects(
1213 &mut self,
1214 ctx: &ExecutorContext,
1215 _version: Version,
1216 sketch: ObjectId,
1217 constraint_ids: Vec<ObjectId>,
1218 segment_ids: Vec<ObjectId>,
1219 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1220 let sketch_block_ref =
1222 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1223
1224 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1226 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1227
1228 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1231
1232 for segment_id in segment_ids_set.iter().copied() {
1233 let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1234 let ObjectKind::Segment { segment } = &segment_object.kind else {
1235 return None;
1236 };
1237 match segment {
1238 Segment::Point(point) => point.owner,
1239 Segment::Line(line) => line.owner,
1240 _ => None,
1241 }
1242 });
1243
1244 if let Some(owner_id) = owner_id
1245 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1246 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1247 && matches!(
1248 owner_segment,
1249 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1250 )
1251 {
1252 resolved_segment_ids_to_delete.insert(owner_id);
1254 } else {
1255 resolved_segment_ids_to_delete.insert(segment_id);
1257 }
1258 }
1259 let referenced_constraint_ids = self
1260 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1261 .map_err(KclErrorWithOutputs::no_outputs)?;
1262
1263 let mut new_ast = self.program.ast.clone();
1264
1265 for constraint_id in referenced_constraint_ids {
1266 if constraint_ids_set.contains(&constraint_id) {
1267 continue;
1268 }
1269
1270 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1271 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1272 })?;
1273 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1274 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1275 "Object is not a constraint, it is {}",
1276 constraint_object.kind.human_friendly_kind_with_article()
1277 ))));
1278 };
1279
1280 match constraint {
1281 Constraint::Coincident(coincident) => {
1282 let remaining_segments =
1283 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1284
1285 if remaining_segments.len() >= 2 {
1287 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1288 .map_err(KclErrorWithOutputs::no_outputs)?;
1289 } else {
1290 constraint_ids_set.insert(constraint_id);
1291 }
1292 }
1293 Constraint::EqualRadius(equal_radius) => {
1294 let remaining_input = equal_radius
1295 .input
1296 .iter()
1297 .copied()
1298 .filter(|segment_id| {
1299 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1300 })
1301 .collect::<Vec<_>>();
1302
1303 if remaining_input.len() >= 2 {
1304 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1305 .map_err(KclErrorWithOutputs::no_outputs)?;
1306 } else {
1307 constraint_ids_set.insert(constraint_id);
1308 }
1309 }
1310 Constraint::LinesEqualLength(lines_equal_length) => {
1311 let remaining_lines = lines_equal_length
1312 .lines
1313 .iter()
1314 .copied()
1315 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1316 .collect::<Vec<_>>();
1317
1318 if remaining_lines.len() >= 2 {
1320 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1321 .map_err(KclErrorWithOutputs::no_outputs)?;
1322 } else {
1323 constraint_ids_set.insert(constraint_id);
1324 }
1325 }
1326 Constraint::Parallel(parallel) => {
1327 let remaining_lines = parallel
1328 .lines
1329 .iter()
1330 .copied()
1331 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1332 .collect::<Vec<_>>();
1333
1334 if remaining_lines.len() >= 2 {
1335 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1336 .map_err(KclErrorWithOutputs::no_outputs)?;
1337 } else {
1338 constraint_ids_set.insert(constraint_id);
1339 }
1340 }
1341 Constraint::Horizontal(Horizontal::Points { points }) => {
1342 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1343
1344 if remaining_points.len() >= 2 {
1345 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1346 .map_err(KclErrorWithOutputs::no_outputs)?;
1347 } else {
1348 constraint_ids_set.insert(constraint_id);
1349 }
1350 }
1351 Constraint::Vertical(Vertical::Points { points }) => {
1352 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1353
1354 if remaining_points.len() >= 2 {
1355 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1356 .map_err(KclErrorWithOutputs::no_outputs)?;
1357 } else {
1358 constraint_ids_set.insert(constraint_id);
1359 }
1360 }
1361 Constraint::Fixed(fixed) => {
1362 if fixed.points.iter().any(|fixed_point| {
1363 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1364 }) {
1365 constraint_ids_set.insert(constraint_id);
1366 }
1367 }
1368 _ => {
1369 constraint_ids_set.insert(constraint_id);
1371 }
1372 }
1373 }
1374
1375 for constraint_id in constraint_ids_set {
1376 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1377 .map_err(KclErrorWithOutputs::no_outputs)?;
1378 }
1379 for segment_id in resolved_segment_ids_to_delete {
1380 self.delete_segment(&mut new_ast, sketch, segment_id)
1381 .map_err(KclErrorWithOutputs::no_outputs)?;
1382 }
1383
1384 self.execute_after_edit(
1385 ctx,
1386 sketch,
1387 sketch_block_ref,
1388 &mut new_ast,
1389 ExecuteAfterEditOptions {
1390 segment_ids_edited: Default::default(),
1391 edit_kind: EditDeleteKind::DeleteNonSketch,
1392 commit_solved_initial_guesses: true,
1393 },
1394 )
1395 .await
1396 }
1397
1398 async fn add_constraint(
1399 &mut self,
1400 ctx: &ExecutorContext,
1401 _version: Version,
1402 sketch: ObjectId,
1403 constraint: Constraint,
1404 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1405 let original_program = self.program.clone();
1409 let original_scene_graph = self.scene_graph.clone();
1410
1411 let mut new_ast = self.program.ast.clone();
1412 let sketch_block_ref = match constraint {
1413 Constraint::Coincident(coincident) => self
1414 .add_coincident(sketch, coincident, &mut new_ast)
1415 .await
1416 .map_err(KclErrorWithOutputs::no_outputs)?,
1417 Constraint::Distance(distance) => self
1418 .add_distance(sketch, distance, &mut new_ast)
1419 .await
1420 .map_err(KclErrorWithOutputs::no_outputs)?,
1421 Constraint::EqualRadius(equal_radius) => self
1422 .add_equal_radius(sketch, equal_radius, &mut new_ast)
1423 .await
1424 .map_err(KclErrorWithOutputs::no_outputs)?,
1425 Constraint::Fixed(fixed) => self
1426 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1427 .await
1428 .map_err(KclErrorWithOutputs::no_outputs)?,
1429 Constraint::HorizontalDistance(distance) => self
1430 .add_horizontal_distance(sketch, distance, &mut new_ast)
1431 .await
1432 .map_err(KclErrorWithOutputs::no_outputs)?,
1433 Constraint::VerticalDistance(distance) => self
1434 .add_vertical_distance(sketch, distance, &mut new_ast)
1435 .await
1436 .map_err(KclErrorWithOutputs::no_outputs)?,
1437 Constraint::Horizontal(horizontal) => self
1438 .add_horizontal(sketch, horizontal, &mut new_ast)
1439 .await
1440 .map_err(KclErrorWithOutputs::no_outputs)?,
1441 Constraint::LinesEqualLength(lines_equal_length) => self
1442 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1443 .await
1444 .map_err(KclErrorWithOutputs::no_outputs)?,
1445 Constraint::Midpoint(midpoint) => self
1446 .add_midpoint(sketch, midpoint, &mut new_ast)
1447 .await
1448 .map_err(KclErrorWithOutputs::no_outputs)?,
1449 Constraint::Parallel(parallel) => self
1450 .add_parallel(sketch, parallel, &mut new_ast)
1451 .await
1452 .map_err(KclErrorWithOutputs::no_outputs)?,
1453 Constraint::Perpendicular(perpendicular) => self
1454 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1455 .await
1456 .map_err(KclErrorWithOutputs::no_outputs)?,
1457 Constraint::Radius(radius) => self
1458 .add_radius(sketch, radius, &mut new_ast)
1459 .await
1460 .map_err(KclErrorWithOutputs::no_outputs)?,
1461 Constraint::Diameter(diameter) => self
1462 .add_diameter(sketch, diameter, &mut new_ast)
1463 .await
1464 .map_err(KclErrorWithOutputs::no_outputs)?,
1465 Constraint::Symmetric(symmetric) => self
1466 .add_symmetric(sketch, symmetric, &mut new_ast)
1467 .await
1468 .map_err(KclErrorWithOutputs::no_outputs)?,
1469 Constraint::Vertical(vertical) => self
1470 .add_vertical(sketch, vertical, &mut new_ast)
1471 .await
1472 .map_err(KclErrorWithOutputs::no_outputs)?,
1473 Constraint::Angle(lines_at_angle) => self
1474 .add_angle(sketch, lines_at_angle, &mut new_ast)
1475 .await
1476 .map_err(KclErrorWithOutputs::no_outputs)?,
1477 Constraint::Tangent(tangent) => self
1478 .add_tangent(sketch, tangent, &mut new_ast)
1479 .await
1480 .map_err(KclErrorWithOutputs::no_outputs)?,
1481 };
1482
1483 let result = self
1484 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1485 .await;
1486
1487 if result.is_err() {
1489 self.program = original_program;
1490 self.scene_graph = original_scene_graph;
1491 }
1492
1493 result
1494 }
1495
1496 async fn chain_segment(
1497 &mut self,
1498 ctx: &ExecutorContext,
1499 version: Version,
1500 sketch: ObjectId,
1501 previous_segment_end_point_id: ObjectId,
1502 segment: SegmentCtor,
1503 _label: Option<String>,
1504 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1505 let SegmentCtor::Line(line_ctor) = segment else {
1509 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1510 "chain_segment currently only supports Line segments, got {}",
1511 segment.human_friendly_kind_with_article(),
1512 ))));
1513 };
1514
1515 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1517
1518 let new_line_id = first_scene_delta
1521 .new_objects
1522 .iter()
1523 .find(|&obj_id| {
1524 let obj = self.scene_graph.objects.get(obj_id.0);
1525 if let Some(obj) = obj {
1526 matches!(
1527 &obj.kind,
1528 ObjectKind::Segment {
1529 segment: Segment::Line(_)
1530 }
1531 )
1532 } else {
1533 false
1534 }
1535 })
1536 .ok_or_else(|| {
1537 KclErrorWithOutputs::no_outputs(KclError::refactor(
1538 "Failed to find new line segment in scene graph".to_string(),
1539 ))
1540 })?;
1541
1542 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1543 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1544 "New line object not found: {new_line_id:?}"
1545 )))
1546 })?;
1547
1548 let ObjectKind::Segment {
1549 segment: new_line_segment,
1550 } = &new_line_obj.kind
1551 else {
1552 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1553 "Object is not a segment: {new_line_obj:?}"
1554 ))));
1555 };
1556
1557 let Segment::Line(new_line) = new_line_segment else {
1558 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1559 "Segment is not a line: {new_line_segment:?}"
1560 ))));
1561 };
1562
1563 let new_line_start_point_id = new_line.start;
1564
1565 let coincident = Coincident {
1567 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1568 };
1569
1570 let (final_src_delta, final_scene_delta) = self
1571 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1572 .await?;
1573
1574 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1577 combined_new_objects.extend(final_scene_delta.new_objects);
1578
1579 let scene_graph_delta = SceneGraphDelta {
1580 new_graph: self.scene_graph_for_ui(),
1581 invalidates_ids: false,
1582 new_objects: combined_new_objects,
1583 exec_outcome: final_scene_delta.exec_outcome,
1584 };
1585
1586 Ok((final_src_delta, scene_graph_delta))
1587 }
1588
1589 async fn edit_constraint_value(
1591 &mut self,
1592 ctx: &ExecutorContext,
1593 _version: Version,
1594 sketch: ObjectId,
1595 constraint_id: ObjectId,
1596 value_expression: String,
1597 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1598 let sketch_block_ref =
1600 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1601
1602 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1603 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1604 })?;
1605 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1606 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1607 "Object is not a constraint: {constraint_id:?}"
1608 ))));
1609 }
1610
1611 let mut new_ast = self.program.ast.clone();
1612
1613 let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1615 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1616 "Invalid constraint value",
1617 &e,
1618 )))
1619 })?;
1620 if !errors.is_empty() {
1621 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1622 format_compilation_issues("Invalid constraint value", &errors),
1623 )));
1624 }
1625 let mut parsed = parsed.ok_or_else(|| {
1626 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1627 })?;
1628 if parsed.ast.body.is_empty() {
1629 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1630 "Empty value expression".to_string(),
1631 )));
1632 }
1633 let first = parsed.ast.body.remove(0);
1634 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1635 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1636 "Value expression must be a simple expression".to_string(),
1637 )));
1638 };
1639
1640 let new_value: ast::BinaryPart = expr_stmt
1641 .inner
1642 .expression
1643 .try_into()
1644 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1645
1646 self.mutate_ast(
1647 &mut new_ast,
1648 constraint_id,
1649 AstMutateCommand::EditConstraintValue { value: new_value },
1650 )
1651 .map_err(KclErrorWithOutputs::no_outputs)?;
1652
1653 self.execute_after_edit(
1654 ctx,
1655 sketch,
1656 sketch_block_ref,
1657 &mut new_ast,
1658 ExecuteAfterEditOptions {
1659 segment_ids_edited: Default::default(),
1660 edit_kind: EditDeleteKind::Edit,
1661 commit_solved_initial_guesses: true,
1662 },
1663 )
1664 .await
1665 }
1666
1667 async fn edit_distance_constraint_label_position(
1668 &mut self,
1669 ctx: &ExecutorContext,
1670 _version: Version,
1671 sketch: ObjectId,
1672 constraint_id: ObjectId,
1673 label_position: Point2d<Number>,
1674 anchor_segment_ids: Vec<ObjectId>,
1675 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1676 let sketch_block_ref =
1678 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1679
1680 let mut new_ast = self.program.ast.clone();
1681 self.mutate_constraint_label_position(&mut new_ast, constraint_id, label_position)
1682 .map_err(KclErrorWithOutputs::no_outputs)?;
1683 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1684
1685 self.execute_after_edit(
1686 ctx,
1687 sketch,
1688 sketch_block_ref,
1689 &mut new_ast,
1690 ExecuteAfterEditOptions {
1691 segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1692 edit_kind: EditDeleteKind::Edit,
1693 commit_solved_initial_guesses,
1694 },
1695 )
1696 .await
1697 }
1698
1699 async fn batch_split_segment_operations(
1707 &mut self,
1708 ctx: &ExecutorContext,
1709 _version: Version,
1710 sketch: ObjectId,
1711 edit_segments: Vec<ExistingSegmentCtor>,
1712 add_constraints: Vec<Constraint>,
1713 delete_constraint_ids: Vec<ObjectId>,
1714 _new_segment_info: sketch::NewSegmentInfo,
1715 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1716 let sketch_block_ref =
1718 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1719
1720 let mut new_ast = self.program.ast.clone();
1721 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1722
1723 for segment in edit_segments {
1725 segment_ids_edited.insert(segment.id);
1726 match segment.ctor {
1727 SegmentCtor::Point(ctor) => self
1728 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1729 .map_err(KclErrorWithOutputs::no_outputs)?,
1730 SegmentCtor::Line(ctor) => self
1731 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1732 .map_err(KclErrorWithOutputs::no_outputs)?,
1733 SegmentCtor::Arc(ctor) => self
1734 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1735 .map_err(KclErrorWithOutputs::no_outputs)?,
1736 SegmentCtor::Circle(ctor) => self
1737 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1738 .map_err(KclErrorWithOutputs::no_outputs)?,
1739 SegmentCtor::ControlPointSpline(ctor) => self
1740 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1741 .map_err(KclErrorWithOutputs::no_outputs)?,
1742 }
1743 }
1744
1745 for constraint in add_constraints {
1747 match constraint {
1748 Constraint::Coincident(coincident) => {
1749 self.add_coincident(sketch, coincident, &mut new_ast)
1750 .await
1751 .map_err(KclErrorWithOutputs::no_outputs)?;
1752 }
1753 Constraint::Distance(distance) => {
1754 self.add_distance(sketch, distance, &mut new_ast)
1755 .await
1756 .map_err(KclErrorWithOutputs::no_outputs)?;
1757 }
1758 Constraint::EqualRadius(equal_radius) => {
1759 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1760 .await
1761 .map_err(KclErrorWithOutputs::no_outputs)?;
1762 }
1763 Constraint::Fixed(fixed) => {
1764 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1765 .await
1766 .map_err(KclErrorWithOutputs::no_outputs)?;
1767 }
1768 Constraint::HorizontalDistance(distance) => {
1769 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1770 .await
1771 .map_err(KclErrorWithOutputs::no_outputs)?;
1772 }
1773 Constraint::VerticalDistance(distance) => {
1774 self.add_vertical_distance(sketch, distance, &mut new_ast)
1775 .await
1776 .map_err(KclErrorWithOutputs::no_outputs)?;
1777 }
1778 Constraint::Horizontal(horizontal) => {
1779 self.add_horizontal(sketch, horizontal, &mut new_ast)
1780 .await
1781 .map_err(KclErrorWithOutputs::no_outputs)?;
1782 }
1783 Constraint::LinesEqualLength(lines_equal_length) => {
1784 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1785 .await
1786 .map_err(KclErrorWithOutputs::no_outputs)?;
1787 }
1788 Constraint::Midpoint(midpoint) => {
1789 self.add_midpoint(sketch, midpoint, &mut new_ast)
1790 .await
1791 .map_err(KclErrorWithOutputs::no_outputs)?;
1792 }
1793 Constraint::Parallel(parallel) => {
1794 self.add_parallel(sketch, parallel, &mut new_ast)
1795 .await
1796 .map_err(KclErrorWithOutputs::no_outputs)?;
1797 }
1798 Constraint::Perpendicular(perpendicular) => {
1799 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1800 .await
1801 .map_err(KclErrorWithOutputs::no_outputs)?;
1802 }
1803 Constraint::Vertical(vertical) => {
1804 self.add_vertical(sketch, vertical, &mut new_ast)
1805 .await
1806 .map_err(KclErrorWithOutputs::no_outputs)?;
1807 }
1808 Constraint::Diameter(diameter) => {
1809 self.add_diameter(sketch, diameter, &mut new_ast)
1810 .await
1811 .map_err(KclErrorWithOutputs::no_outputs)?;
1812 }
1813 Constraint::Radius(radius) => {
1814 self.add_radius(sketch, radius, &mut new_ast)
1815 .await
1816 .map_err(KclErrorWithOutputs::no_outputs)?;
1817 }
1818 Constraint::Symmetric(symmetric) => {
1819 self.add_symmetric(sketch, symmetric, &mut new_ast)
1820 .await
1821 .map_err(KclErrorWithOutputs::no_outputs)?;
1822 }
1823 Constraint::Angle(angle) => {
1824 self.add_angle(sketch, angle, &mut new_ast)
1825 .await
1826 .map_err(KclErrorWithOutputs::no_outputs)?;
1827 }
1828 Constraint::Tangent(tangent) => {
1829 self.add_tangent(sketch, tangent, &mut new_ast)
1830 .await
1831 .map_err(KclErrorWithOutputs::no_outputs)?;
1832 }
1833 }
1834 }
1835
1836 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1838
1839 let has_constraint_deletions = !constraint_ids_set.is_empty();
1840 for constraint_id in constraint_ids_set {
1841 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1842 .map_err(KclErrorWithOutputs::no_outputs)?;
1843 }
1844
1845 let (source_delta, mut scene_graph_delta) = self
1849 .execute_after_edit(
1850 ctx,
1851 sketch,
1852 sketch_block_ref,
1853 &mut new_ast,
1854 ExecuteAfterEditOptions {
1855 segment_ids_edited,
1856 edit_kind: EditDeleteKind::Edit,
1857 commit_solved_initial_guesses: true,
1858 },
1859 )
1860 .await?;
1861
1862 if has_constraint_deletions {
1865 scene_graph_delta.invalidates_ids = true;
1866 }
1867
1868 Ok((source_delta, scene_graph_delta))
1869 }
1870
1871 async fn batch_tail_cut_operations(
1872 &mut self,
1873 ctx: &ExecutorContext,
1874 _version: Version,
1875 sketch: ObjectId,
1876 edit_segments: Vec<ExistingSegmentCtor>,
1877 add_constraints: Vec<Constraint>,
1878 delete_constraint_ids: Vec<ObjectId>,
1879 additional_edited_segment_ids: Vec<ObjectId>,
1880 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1881 let sketch_block_ref =
1882 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1883
1884 let mut new_ast = self.program.ast.clone();
1885 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1886
1887 for segment in edit_segments {
1889 segment_ids_edited.insert(segment.id);
1890 match segment.ctor {
1891 SegmentCtor::Point(ctor) => self
1892 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1893 .map_err(KclErrorWithOutputs::no_outputs)?,
1894 SegmentCtor::Line(ctor) => self
1895 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1896 .map_err(KclErrorWithOutputs::no_outputs)?,
1897 SegmentCtor::Arc(ctor) => self
1898 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1899 .map_err(KclErrorWithOutputs::no_outputs)?,
1900 SegmentCtor::Circle(ctor) => self
1901 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1902 .map_err(KclErrorWithOutputs::no_outputs)?,
1903 SegmentCtor::ControlPointSpline(ctor) => self
1904 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1905 .map_err(KclErrorWithOutputs::no_outputs)?,
1906 }
1907 }
1908
1909 segment_ids_edited.extend(additional_edited_segment_ids);
1910
1911 for constraint in add_constraints {
1913 match constraint {
1914 Constraint::Coincident(coincident) => {
1915 self.add_coincident(sketch, coincident, &mut new_ast)
1916 .await
1917 .map_err(KclErrorWithOutputs::no_outputs)?;
1918 }
1919 other => {
1920 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1921 "unsupported constraint in tail cut batch: {other:?}"
1922 ))));
1923 }
1924 }
1925 }
1926
1927 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1929
1930 let has_constraint_deletions = !constraint_ids_set.is_empty();
1931 for constraint_id in constraint_ids_set {
1932 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1933 .map_err(KclErrorWithOutputs::no_outputs)?;
1934 }
1935
1936 let (source_delta, mut scene_graph_delta) = self
1940 .execute_after_edit(
1941 ctx,
1942 sketch,
1943 sketch_block_ref,
1944 &mut new_ast,
1945 ExecuteAfterEditOptions {
1946 segment_ids_edited,
1947 edit_kind: EditDeleteKind::Edit,
1948 commit_solved_initial_guesses: true,
1949 },
1950 )
1951 .await?;
1952
1953 if has_constraint_deletions {
1956 scene_graph_delta.invalidates_ids = true;
1957 }
1958
1959 Ok((source_delta, scene_graph_delta))
1960 }
1961}
1962
1963impl FrontendState {
1964 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1965 self.program = program.clone();
1966
1967 self.point_freedom_cache.clear();
1978 match ctx.run_with_caching(program).await {
1979 Ok(outcome) => {
1980 let outcome = self.update_state_after_exec(outcome, true);
1981 let checkpoint_id = self
1982 .create_sketch_checkpoint(outcome.clone())
1983 .await
1984 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1985 Ok(SetProgramOutcome::Success {
1986 scene_graph: Box::new(self.scene_graph_for_ui()),
1987 exec_outcome: Box::new(outcome),
1988 checkpoint_id: Some(checkpoint_id),
1989 })
1990 }
1991 Err(mut err) => {
1992 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1995 self.update_state_after_exec(outcome, true);
1996 err.scene_graph = Some(self.scene_graph_for_ui());
1997 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1998 }
1999 }
2000 }
2001
2002 pub async fn engine_execute(
2005 &mut self,
2006 ctx: &ExecutorContext,
2007 program: Program,
2008 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
2009 self.program = program.clone();
2010
2011 self.point_freedom_cache.clear();
2015 match ctx.run_with_caching(program).await {
2016 Ok(outcome) => {
2017 let outcome = self.update_state_after_exec(outcome, true);
2018 Ok(SceneGraphDelta {
2019 new_graph: self.scene_graph_for_ui(),
2020 exec_outcome: outcome,
2021 new_objects: Default::default(),
2023 invalidates_ids: Default::default(),
2025 })
2026 }
2027 Err(mut err) => {
2028 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2030 self.update_state_after_exec(outcome, true);
2031 err.scene_graph = Some(self.scene_graph_for_ui());
2032 Err(err)
2033 }
2034 }
2035 }
2036
2037 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
2038 if matches!(err.error, KclError::EngineHangup { .. }) {
2039 return Err(err);
2043 }
2044
2045 let KclErrorWithOutputs {
2046 error,
2047 mut non_fatal,
2048 variables,
2049 operations,
2050 artifact_graph,
2051 scene_objects,
2052 source_range_to_object,
2053 var_solutions,
2054 refactor_metadata,
2055 filenames,
2056 source_files,
2057 default_planes,
2058 ..
2059 } = err;
2060
2061 non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2062
2063 Ok(ExecOutcome {
2064 variables,
2065 #[cfg(test)]
2066 test_program_memory: Default::default(),
2067 filenames,
2068 operations,
2069 artifact_graph,
2070 scene_objects,
2071 source_range_to_object,
2072 var_solutions,
2073 refactor_metadata,
2074 issues: non_fatal,
2075 source_files,
2076 default_planes,
2077 })
2078 }
2079
2080 async fn add_point(
2081 &mut self,
2082 ctx: &ExecutorContext,
2083 sketch: ObjectId,
2084 ctor: PointCtor,
2085 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2086 let at_ast = to_ast_point2d(&ctor.position)
2088 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2089 let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2090 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2091 unlabeled: None,
2092 arguments: vec![ast::LabeledArg {
2093 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2094 arg: at_ast,
2095 }],
2096 digest: None,
2097 non_code_meta: Default::default(),
2098 })));
2099
2100 let sketch_id = sketch;
2102 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2103 #[cfg(target_arch = "wasm32")]
2104 web_sys::console::error_1(
2105 &format!(
2106 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2107 self.scene_graph.objects
2108 )
2109 .into(),
2110 );
2111 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2112 })?;
2113 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2114 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2115 "Object is not a sketch, it is {}",
2116 sketch_object.kind.human_friendly_kind_with_article(),
2117 ))));
2118 };
2119 let mut new_ast = self.program.ast.clone();
2121 let (sketch_block_ref, _) = self
2122 .mutate_ast(
2123 &mut new_ast,
2124 sketch_id,
2125 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2126 )
2127 .map_err(KclErrorWithOutputs::no_outputs)?;
2128 let new_source = source_from_ast(&new_ast);
2130 let new_program = parse_frontend_mutation_source(
2132 &new_source,
2133 "Error parsing KCL source after adding point",
2134 "No AST produced after adding point",
2135 )?;
2136
2137 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2138 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2139 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2140 )))
2141 })?;
2142
2143 self.program = new_program.clone();
2145
2146 let mut truncated_program = new_program;
2148 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2149 .map_err(KclErrorWithOutputs::no_outputs)?;
2150
2151 let outcome = ctx
2153 .run_mock(
2154 &truncated_program,
2155 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2156 )
2157 .await?;
2158
2159 let new_object_ids = {
2160 let make_err =
2161 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2162 let segment_id = outcome
2163 .source_range_to_object
2164 .get(&point_node_ref.range)
2165 .copied()
2166 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2167 let segment_object = outcome
2168 .scene_objects
2169 .get(segment_id.0)
2170 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2171 let ObjectKind::Segment { segment } = &segment_object.kind else {
2172 return Err(make_err(format!(
2173 "Object is not a segment, it is {}",
2174 segment_object.kind.human_friendly_kind_with_article()
2175 )));
2176 };
2177 let Segment::Point(_) = segment else {
2178 return Err(make_err(format!(
2179 "Segment is not a point, it is {}",
2180 segment.human_friendly_kind_with_article()
2181 )));
2182 };
2183 vec![segment_id]
2184 };
2185 let src_delta = SourceDelta { text: new_source };
2186 let outcome = self.update_state_after_exec(outcome, false);
2188 let scene_graph_delta = SceneGraphDelta {
2189 new_graph: self.scene_graph_for_ui(),
2190 invalidates_ids: false,
2191 new_objects: new_object_ids,
2192 exec_outcome: outcome,
2193 };
2194 Ok((src_delta, scene_graph_delta))
2195 }
2196
2197 async fn add_line(
2198 &mut self,
2199 ctx: &ExecutorContext,
2200 sketch: ObjectId,
2201 ctor: LineCtor,
2202 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2203 let start_ast = to_ast_point2d(&ctor.start)
2205 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2206 let end_ast = to_ast_point2d(&ctor.end)
2207 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2208 let mut arguments = vec![
2209 ast::LabeledArg {
2210 label: Some(ast::Identifier::new(LINE_START_PARAM)),
2211 arg: start_ast,
2212 },
2213 ast::LabeledArg {
2214 label: Some(ast::Identifier::new(LINE_END_PARAM)),
2215 arg: end_ast,
2216 },
2217 ];
2218 if ctor.construction == Some(true) {
2220 arguments.push(ast::LabeledArg {
2221 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2222 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2223 value: ast::LiteralValue::Bool(true),
2224 raw: "true".to_string(),
2225 digest: None,
2226 }))),
2227 });
2228 }
2229 let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2230 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2231 unlabeled: None,
2232 arguments,
2233 digest: None,
2234 non_code_meta: Default::default(),
2235 })));
2236
2237 let sketch_id = sketch;
2239 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2240 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2241 })?;
2242 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2243 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2244 "Object is not a sketch, it is {}",
2245 sketch_object.kind.human_friendly_kind_with_article(),
2246 ))));
2247 };
2248 let mut new_ast = self.program.ast.clone();
2250 let (sketch_block_ref, _) = self
2251 .mutate_ast(
2252 &mut new_ast,
2253 sketch_id,
2254 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2255 )
2256 .map_err(KclErrorWithOutputs::no_outputs)?;
2257 let new_source = source_from_ast(&new_ast);
2259 let new_program = parse_frontend_mutation_source(
2261 &new_source,
2262 "Error parsing KCL source after adding line",
2263 "No AST produced after adding line",
2264 )?;
2265
2266 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2267 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2268 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2269 )))
2270 })?;
2271
2272 self.program = new_program.clone();
2274
2275 let mut truncated_program = new_program;
2277 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2278 .map_err(KclErrorWithOutputs::no_outputs)?;
2279
2280 let outcome = ctx
2282 .run_mock(
2283 &truncated_program,
2284 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2285 )
2286 .await?;
2287
2288 let new_object_ids = {
2289 let make_err =
2290 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2291 let segment_id = outcome
2292 .source_range_to_object
2293 .get(&line_node_ref.range)
2294 .copied()
2295 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2296 let segment_object = outcome
2297 .scene_object_by_id(segment_id)
2298 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2299 let ObjectKind::Segment { segment } = &segment_object.kind else {
2300 return Err(make_err(format!(
2301 "Object is not a segment, it is {}",
2302 segment_object.kind.human_friendly_kind_with_article()
2303 )));
2304 };
2305 let Segment::Line(line) = segment else {
2306 return Err(make_err(format!(
2307 "Segment is not a line, it is {}",
2308 segment.human_friendly_kind_with_article()
2309 )));
2310 };
2311 vec![line.start, line.end, segment_id]
2312 };
2313 let src_delta = SourceDelta { text: new_source };
2314 let outcome = self.update_state_after_exec(outcome, false);
2316 let scene_graph_delta = SceneGraphDelta {
2317 new_graph: self.scene_graph_for_ui(),
2318 invalidates_ids: false,
2319 new_objects: new_object_ids,
2320 exec_outcome: outcome,
2321 };
2322 Ok((src_delta, scene_graph_delta))
2323 }
2324
2325 async fn add_arc(
2326 &mut self,
2327 ctx: &ExecutorContext,
2328 sketch: ObjectId,
2329 ctor: ArcCtor,
2330 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2331 let start_ast = to_ast_point2d(&ctor.start)
2333 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2334 let end_ast = to_ast_point2d(&ctor.end)
2335 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2336 let center_ast = to_ast_point2d(&ctor.center)
2337 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2338 let mut arguments = vec![
2339 ast::LabeledArg {
2340 label: Some(ast::Identifier::new(ARC_START_PARAM)),
2341 arg: start_ast,
2342 },
2343 ast::LabeledArg {
2344 label: Some(ast::Identifier::new(ARC_END_PARAM)),
2345 arg: end_ast,
2346 },
2347 ast::LabeledArg {
2348 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2349 arg: center_ast,
2350 },
2351 ];
2352 if ctor.direction == Some(ArcDirection::Cw) {
2355 arguments.push(ast::LabeledArg {
2356 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2357 arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2358 });
2359 }
2360 if ctor.construction == Some(true) {
2362 arguments.push(ast::LabeledArg {
2363 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2364 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2365 value: ast::LiteralValue::Bool(true),
2366 raw: "true".to_string(),
2367 digest: None,
2368 }))),
2369 });
2370 }
2371 let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2372 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2373 unlabeled: None,
2374 arguments,
2375 digest: None,
2376 non_code_meta: Default::default(),
2377 })));
2378
2379 let sketch_id = sketch;
2381 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2382 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2383 })?;
2384 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2385 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2386 "Object is not a sketch, it is {}",
2387 sketch_object.kind.human_friendly_kind_with_article(),
2388 ))));
2389 };
2390 let mut new_ast = self.program.ast.clone();
2392 let (sketch_block_ref, _) = self
2393 .mutate_ast(
2394 &mut new_ast,
2395 sketch_id,
2396 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2397 )
2398 .map_err(KclErrorWithOutputs::no_outputs)?;
2399 let new_source = source_from_ast(&new_ast);
2401 let new_program = parse_frontend_mutation_source(
2403 &new_source,
2404 "Error parsing KCL source after adding arc",
2405 "No AST produced after adding arc",
2406 )?;
2407
2408 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2409 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2410 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2411 )))
2412 })?;
2413
2414 self.program = new_program.clone();
2416
2417 let mut truncated_program = new_program;
2419 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2420 .map_err(KclErrorWithOutputs::no_outputs)?;
2421
2422 let outcome = ctx
2424 .run_mock(
2425 &truncated_program,
2426 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2427 )
2428 .await?;
2429
2430 let new_object_ids = {
2431 let make_err =
2432 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2433 let segment_id = outcome
2434 .source_range_to_object
2435 .get(&arc_node_ref.range)
2436 .copied()
2437 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2438 let segment_object = outcome
2439 .scene_objects
2440 .get(segment_id.0)
2441 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2442 let ObjectKind::Segment { segment } = &segment_object.kind else {
2443 return Err(make_err(format!(
2444 "Object is not a segment, it is {}",
2445 segment_object.kind.human_friendly_kind_with_article()
2446 )));
2447 };
2448 let Segment::Arc(arc) = segment else {
2449 return Err(make_err(format!(
2450 "Segment is not an arc, it is {}",
2451 segment.human_friendly_kind_with_article()
2452 )));
2453 };
2454 vec![arc.start, arc.end, arc.center, segment_id]
2455 };
2456 let src_delta = SourceDelta { text: new_source };
2457 let outcome = self.update_state_after_exec(outcome, false);
2459 let scene_graph_delta = SceneGraphDelta {
2460 new_graph: self.scene_graph_for_ui(),
2461 invalidates_ids: false,
2462 new_objects: new_object_ids,
2463 exec_outcome: outcome,
2464 };
2465 Ok((src_delta, scene_graph_delta))
2466 }
2467
2468 async fn add_circle(
2469 &mut self,
2470 ctx: &ExecutorContext,
2471 sketch: ObjectId,
2472 ctor: CircleCtor,
2473 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2474 let start_ast = to_ast_point2d(&ctor.start)
2476 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2477 let center_ast = to_ast_point2d(&ctor.center)
2478 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2479 let mut arguments = vec![
2480 ast::LabeledArg {
2481 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2482 arg: start_ast,
2483 },
2484 ast::LabeledArg {
2485 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2486 arg: center_ast,
2487 },
2488 ];
2489 if ctor.construction == Some(true) {
2491 arguments.push(ast::LabeledArg {
2492 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2493 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2494 value: ast::LiteralValue::Bool(true),
2495 raw: "true".to_string(),
2496 digest: None,
2497 }))),
2498 });
2499 }
2500 let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2501 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2502 unlabeled: None,
2503 arguments,
2504 digest: None,
2505 non_code_meta: Default::default(),
2506 })));
2507
2508 let sketch_id = sketch;
2510 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2511 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2512 })?;
2513 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2514 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2515 "Object is not a sketch, it is {}",
2516 sketch_object.kind.human_friendly_kind_with_article(),
2517 ))));
2518 };
2519 let mut new_ast = self.program.ast.clone();
2521 let (sketch_block_ref, _) = self
2522 .mutate_ast(
2523 &mut new_ast,
2524 sketch_id,
2525 AstMutateCommand::AddSketchBlockVarDecl {
2526 prefix: CIRCLE_VARIABLE.to_owned(),
2527 expr: circle_ast,
2528 },
2529 )
2530 .map_err(KclErrorWithOutputs::no_outputs)?;
2531 let new_source = source_from_ast(&new_ast);
2533 let new_program = parse_frontend_mutation_source(
2535 &new_source,
2536 "Error parsing KCL source after adding circle",
2537 "No AST produced after adding circle",
2538 )?;
2539
2540 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2541 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2542 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2543 )))
2544 })?;
2545
2546 self.program = new_program.clone();
2548
2549 let mut truncated_program = new_program;
2551 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2552 .map_err(KclErrorWithOutputs::no_outputs)?;
2553
2554 let outcome = ctx
2556 .run_mock(
2557 &truncated_program,
2558 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2559 )
2560 .await?;
2561
2562 let new_object_ids = {
2563 let make_err =
2564 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2565 let segment_id = outcome
2566 .source_range_to_object
2567 .get(&circle_node_ref.range)
2568 .copied()
2569 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2570 let segment_object = outcome
2571 .scene_objects
2572 .get(segment_id.0)
2573 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2574 let ObjectKind::Segment { segment } = &segment_object.kind else {
2575 return Err(make_err(format!(
2576 "Object is not a segment, it is {}",
2577 segment_object.kind.human_friendly_kind_with_article()
2578 )));
2579 };
2580 let Segment::Circle(circle) = segment else {
2581 return Err(make_err(format!(
2582 "Segment is not a circle, it is {}",
2583 segment.human_friendly_kind_with_article()
2584 )));
2585 };
2586 vec![circle.start, circle.center, segment_id]
2587 };
2588 let src_delta = SourceDelta { text: new_source };
2589 let outcome = self.update_state_after_exec(outcome, false);
2591 let scene_graph_delta = SceneGraphDelta {
2592 new_graph: self.scene_graph_for_ui(),
2593 invalidates_ids: false,
2594 new_objects: new_object_ids,
2595 exec_outcome: outcome,
2596 };
2597 Ok((src_delta, scene_graph_delta))
2598 }
2599
2600 async fn add_control_point_spline(
2601 &mut self,
2602 ctx: &ExecutorContext,
2603 sketch: ObjectId,
2604 ctor: ControlPointSplineCtor,
2605 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2606 let new_program = ensure_control_point_spline_experimental_features(&self.program)
2607 .map_err(KclErrorWithOutputs::no_outputs)?;
2608
2609 let points_ast = to_ast_point2d_array(&ctor.points)
2610 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2611 let mut arguments = vec![ast::LabeledArg {
2612 label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2613 arg: points_ast,
2614 }];
2615 if ctor.construction == Some(true) {
2616 arguments.push(ast::LabeledArg {
2617 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2618 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2619 value: ast::LiteralValue::Bool(true),
2620 raw: "true".to_string(),
2621 digest: None,
2622 }))),
2623 });
2624 }
2625 let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2626 callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2627 unlabeled: None,
2628 arguments,
2629 digest: None,
2630 non_code_meta: Default::default(),
2631 })));
2632
2633 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2634 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2635 })?;
2636 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2637 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2638 "Object is not a sketch, it is {}",
2639 sketch_object.kind.human_friendly_kind_with_article(),
2640 ))));
2641 };
2642
2643 let mut new_ast = new_program.ast.clone();
2644 let (sketch_block_ref, _) = self
2645 .mutate_ast(
2646 &mut new_ast,
2647 sketch,
2648 AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2649 )
2650 .map_err(KclErrorWithOutputs::no_outputs)?;
2651 let new_source = source_from_ast(&new_ast);
2652 let new_program = parse_frontend_mutation_source(
2653 &new_source,
2654 "Error parsing KCL source after adding controlPointSpline",
2655 "No AST produced after adding controlPointSpline",
2656 )?;
2657
2658 let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2659 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2660 "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2661 )))
2662 })?;
2663
2664 self.program = new_program.clone();
2665
2666 let mut truncated_program = new_program;
2667 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2668 .map_err(KclErrorWithOutputs::no_outputs)?;
2669
2670 let outcome = ctx
2671 .run_mock(
2672 &truncated_program,
2673 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2674 )
2675 .await?;
2676
2677 let new_object_ids = {
2678 let make_err =
2679 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2680 let segment_id = outcome
2681 .source_range_to_object
2682 .get(&spline_node_ref.range)
2683 .copied()
2684 .ok_or_else(|| {
2685 make_err(format!(
2686 "Source range of controlPointSpline not found: {spline_node_ref:?}"
2687 ))
2688 })?;
2689 let segment_object = outcome
2690 .scene_objects
2691 .get(segment_id.0)
2692 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2693 let ObjectKind::Segment { segment } = &segment_object.kind else {
2694 return Err(make_err(format!(
2695 "Object is not a segment, it is {}",
2696 segment_object.kind.human_friendly_kind_with_article()
2697 )));
2698 };
2699 let Segment::ControlPointSpline(spline) = segment else {
2700 return Err(make_err(format!(
2701 "Segment is not a control point spline, it is {}",
2702 segment.human_friendly_kind_with_article()
2703 )));
2704 };
2705
2706 let mut ids = outcome
2707 .scene_objects
2708 .iter()
2709 .filter_map(|obj| match &obj.kind {
2710 ObjectKind::Segment {
2711 segment: Segment::Line(line),
2712 } if line.owner == Some(segment_id) => Some(obj.id),
2713 _ => None,
2714 })
2715 .collect::<Vec<_>>();
2716 ids.extend(spline.controls.clone());
2717 ids.push(segment_id);
2718 ids
2719 };
2720 let src_delta = SourceDelta { text: new_source };
2721 let outcome = self.update_state_after_exec(outcome, false);
2722 let scene_graph_delta = SceneGraphDelta {
2723 new_graph: self.scene_graph_for_ui(),
2724 invalidates_ids: false,
2725 new_objects: new_object_ids,
2726 exec_outcome: outcome,
2727 };
2728 Ok((src_delta, scene_graph_delta))
2729 }
2730
2731 fn edit_point(
2732 &mut self,
2733 new_ast: &mut ast::Node<ast::Program>,
2734 sketch: ObjectId,
2735 point: ObjectId,
2736 ctor: PointCtor,
2737 ) -> Result<(), KclError> {
2738 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2740
2741 let sketch_id = sketch;
2743 let sketch_object = self
2744 .scene_graph
2745 .objects
2746 .get(sketch_id.0)
2747 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2748 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2749 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2750 };
2751 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2752 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2753 })?;
2754 let point_id = point;
2756 let point_object = self
2757 .scene_graph
2758 .objects
2759 .get(point_id.0)
2760 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2761 let ObjectKind::Segment {
2762 segment: Segment::Point(point),
2763 } = &point_object.kind
2764 else {
2765 return Err(KclError::refactor(format!(
2766 "Object is not a point segment: {point_object:?}"
2767 )));
2768 };
2769
2770 if let Some(owner_id) = point.owner {
2772 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2773 KclError::refactor(format!(
2774 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2775 ))
2776 })?;
2777 let ObjectKind::Segment { segment } = &owner_object.kind else {
2778 return Err(KclError::refactor(format!(
2779 "Internal: Owner of point is not a segment, but found {}",
2780 owner_object.kind.human_friendly_kind_with_article()
2781 )));
2782 };
2783
2784 if let Segment::Line(line) = segment {
2786 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2787 return Err(KclError::refactor(format!(
2788 "Internal: Owner of point does not have line ctor, but found {}",
2789 line.ctor.human_friendly_kind_with_article()
2790 )));
2791 };
2792 let mut line_ctor = line_ctor.clone();
2793 if line.start == point_id {
2795 line_ctor.start = ctor.position;
2796 } else if line.end == point_id {
2797 line_ctor.end = ctor.position;
2798 } else {
2799 return Err(KclError::refactor(format!(
2800 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2801 )));
2802 }
2803 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2804 }
2805
2806 if let Segment::Arc(arc) = segment {
2808 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2809 return Err(KclError::refactor(format!(
2810 "Internal: Owner of point does not have arc ctor, but found {}",
2811 arc.ctor.human_friendly_kind_with_article()
2812 )));
2813 };
2814 let mut arc_ctor = arc_ctor.clone();
2815 if arc.center == point_id {
2817 arc_ctor.center = ctor.position;
2818 } else if arc.start == point_id {
2819 arc_ctor.start = ctor.position;
2820 } else if arc.end == point_id {
2821 arc_ctor.end = ctor.position;
2822 } else {
2823 return Err(KclError::refactor(format!(
2824 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2825 )));
2826 }
2827 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2828 }
2829
2830 if let Segment::Circle(circle) = segment {
2832 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2833 return Err(KclError::refactor(format!(
2834 "Internal: Owner of point does not have circle ctor, but found {}",
2835 circle.ctor.human_friendly_kind_with_article()
2836 )));
2837 };
2838 let mut circle_ctor = circle_ctor.clone();
2839 if circle.center == point_id {
2840 circle_ctor.center = ctor.position;
2841 } else if circle.start == point_id {
2842 circle_ctor.start = ctor.position;
2843 } else {
2844 return Err(KclError::refactor(format!(
2845 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2846 )));
2847 }
2848 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2849 }
2850
2851 if let Segment::ControlPointSpline(spline) = segment {
2852 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2853 return Err(KclError::refactor(format!(
2854 "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2855 spline.ctor.human_friendly_kind_with_article()
2856 )));
2857 };
2858 let mut spline_ctor = spline_ctor.clone();
2859 let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2860 return Err(KclError::refactor(format!(
2861 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2862 )));
2863 };
2864 spline_ctor.points[control_index] = ctor.position;
2865 return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2866 }
2867
2868 }
2871
2872 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2874 Ok(())
2875 }
2876
2877 fn edit_line(
2878 &mut self,
2879 new_ast: &mut ast::Node<ast::Program>,
2880 sketch: ObjectId,
2881 line: ObjectId,
2882 ctor: LineCtor,
2883 ) -> Result<(), KclError> {
2884 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2886 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2887
2888 let sketch_id = sketch;
2890 let sketch_object = self
2891 .scene_graph
2892 .objects
2893 .get(sketch_id.0)
2894 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2895 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2896 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2897 };
2898 sketch
2899 .segments
2900 .iter()
2901 .find(|o| **o == line)
2902 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2903 let line_id = line;
2905 let line_object = self
2906 .scene_graph
2907 .objects
2908 .get(line_id.0)
2909 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2910 let ObjectKind::Segment { .. } = &line_object.kind else {
2911 let kind = line_object.kind.human_friendly_kind_with_article();
2912 return Err(KclError::refactor(format!(
2913 "This constraint only works on Segments, but you selected {kind}"
2914 )));
2915 };
2916
2917 self.mutate_ast(
2919 new_ast,
2920 line_id,
2921 AstMutateCommand::EditLine {
2922 start: new_start_ast,
2923 end: new_end_ast,
2924 construction: ctor.construction,
2925 },
2926 )?;
2927 Ok(())
2928 }
2929
2930 fn edit_arc(
2931 &mut self,
2932 new_ast: &mut ast::Node<ast::Program>,
2933 sketch: ObjectId,
2934 arc: ObjectId,
2935 ctor: ArcCtor,
2936 ) -> Result<(), KclError> {
2937 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2939 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2940 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2941
2942 let sketch_id = sketch;
2944 let sketch_object = self
2945 .scene_graph
2946 .objects
2947 .get(sketch_id.0)
2948 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2949 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2950 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2951 };
2952 sketch
2953 .segments
2954 .iter()
2955 .find(|o| **o == arc)
2956 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2957 let arc_id = arc;
2959 let arc_object = self
2960 .scene_graph
2961 .objects
2962 .get(arc_id.0)
2963 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2964 let ObjectKind::Segment { .. } = &arc_object.kind else {
2965 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2966 };
2967
2968 self.mutate_ast(
2970 new_ast,
2971 arc_id,
2972 AstMutateCommand::EditArc {
2973 start: new_start_ast,
2974 end: new_end_ast,
2975 center: new_center_ast,
2976 direction: ctor.direction,
2977 construction: ctor.construction,
2978 },
2979 )?;
2980 Ok(())
2981 }
2982
2983 fn edit_circle(
2984 &mut self,
2985 new_ast: &mut ast::Node<ast::Program>,
2986 sketch: ObjectId,
2987 circle: ObjectId,
2988 ctor: CircleCtor,
2989 ) -> Result<(), KclError> {
2990 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2992 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2993
2994 let sketch_id = sketch;
2996 let sketch_object = self
2997 .scene_graph
2998 .objects
2999 .get(sketch_id.0)
3000 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3001 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3002 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3003 };
3004 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3005 KclError::refactor(format!(
3006 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3007 ))
3008 })?;
3009 let circle_id = circle;
3011 let circle_object = self
3012 .scene_graph
3013 .objects
3014 .get(circle_id.0)
3015 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3016 let ObjectKind::Segment { .. } = &circle_object.kind else {
3017 return Err(KclError::refactor(format!(
3018 "Object is not a segment: {circle_object:?}"
3019 )));
3020 };
3021
3022 self.mutate_ast(
3024 new_ast,
3025 circle_id,
3026 AstMutateCommand::EditCircle {
3027 start: new_start_ast,
3028 center: new_center_ast,
3029 construction: ctor.construction,
3030 },
3031 )?;
3032 Ok(())
3033 }
3034
3035 fn edit_control_point_spline(
3036 &mut self,
3037 new_ast: &mut ast::Node<ast::Program>,
3038 sketch: ObjectId,
3039 spline: ObjectId,
3040 ctor: ControlPointSplineCtor,
3041 ) -> Result<(), KclError> {
3042 let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3043
3044 let sketch_object = self
3045 .scene_graph
3046 .objects
3047 .get(sketch.0)
3048 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3049 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3050 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3051 };
3052 sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3053 KclError::refactor(format!(
3054 "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3055 ))
3056 })?;
3057
3058 let spline_object =
3059 self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3060 KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3061 })?;
3062 let ObjectKind::Segment { .. } = &spline_object.kind else {
3063 return Err(KclError::refactor(format!(
3064 "Object is not a segment: {spline_object:?}"
3065 )));
3066 };
3067
3068 self.mutate_ast(
3069 new_ast,
3070 spline,
3071 AstMutateCommand::EditControlPointSpline {
3072 points: points_ast,
3073 construction: ctor.construction,
3074 },
3075 )?;
3076 Ok(())
3077 }
3078
3079 fn delete_segment(
3080 &mut self,
3081 new_ast: &mut ast::Node<ast::Program>,
3082 sketch: ObjectId,
3083 segment_id: ObjectId,
3084 ) -> Result<(), KclError> {
3085 let sketch_id = sketch;
3087 let sketch_object = self
3088 .scene_graph
3089 .objects
3090 .get(sketch_id.0)
3091 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3092 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3093 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3094 };
3095 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3096 KclError::refactor(format!(
3097 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3098 ))
3099 })?;
3100 let segment_object =
3102 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3103 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3104 })?;
3105 let ObjectKind::Segment { .. } = &segment_object.kind else {
3106 return Err(KclError::refactor(format!(
3107 "Object is not a segment, it is {}",
3108 segment_object.kind.human_friendly_kind_with_article()
3109 )));
3110 };
3111
3112 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3114 Ok(())
3115 }
3116
3117 fn delete_constraint(
3118 &mut self,
3119 new_ast: &mut ast::Node<ast::Program>,
3120 sketch: ObjectId,
3121 constraint_id: ObjectId,
3122 ) -> Result<(), KclError> {
3123 let sketch_id = sketch;
3125 let sketch_object = self
3126 .scene_graph
3127 .objects
3128 .get(sketch_id.0)
3129 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3130 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3131 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3132 };
3133 sketch
3134 .constraints
3135 .iter()
3136 .find(|o| **o == constraint_id)
3137 .ok_or_else(|| {
3138 KclError::refactor(format!(
3139 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3140 ))
3141 })?;
3142 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3144 KclError::refactor(format!(
3145 "Constraint not found in scene graph: constraint={constraint_id:?}"
3146 ))
3147 })?;
3148 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3149 return Err(KclError::refactor(format!(
3150 "Object is not a constraint, it is {}",
3151 constraint_object.kind.human_friendly_kind_with_article()
3152 )));
3153 };
3154
3155 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3157 Ok(())
3158 }
3159
3160 fn edit_coincident_constraint(
3161 &mut self,
3162 new_ast: &mut ast::Node<ast::Program>,
3163 constraint_id: ObjectId,
3164 segments: Vec<ConstraintSegment>,
3165 ) -> Result<(), KclError> {
3166 if segments.len() < 2 {
3167 return Err(KclError::refactor(format!(
3168 "Coincident constraint must have at least 2 inputs, got {}",
3169 segments.len()
3170 )));
3171 }
3172
3173 let segment_asts = segments
3174 .iter()
3175 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3176 .collect::<Result<Vec<_>, _>>()?;
3177
3178 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3179 elements: segment_asts,
3180 digest: None,
3181 non_code_meta: Default::default(),
3182 })));
3183
3184 self.mutate_ast(
3185 new_ast,
3186 constraint_id,
3187 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3188 )?;
3189 Ok(())
3190 }
3191
3192 fn edit_horizontal_points_constraint(
3193 &mut self,
3194 new_ast: &mut ast::Node<ast::Program>,
3195 constraint_id: ObjectId,
3196 points: Vec<ConstraintSegment>,
3197 ) -> Result<(), KclError> {
3198 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3199 }
3200
3201 fn edit_vertical_points_constraint(
3202 &mut self,
3203 new_ast: &mut ast::Node<ast::Program>,
3204 constraint_id: ObjectId,
3205 points: Vec<ConstraintSegment>,
3206 ) -> Result<(), KclError> {
3207 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3208 }
3209
3210 fn edit_axis_points_constraint(
3211 &mut self,
3212 new_ast: &mut ast::Node<ast::Program>,
3213 constraint_id: ObjectId,
3214 points: Vec<ConstraintSegment>,
3215 constraint_name: &str,
3216 ) -> Result<(), KclError> {
3217 if points.len() < 2 {
3218 return Err(KclError::refactor(format!(
3219 "{constraint_name} points constraint must have at least 2 points, got {}",
3220 points.len()
3221 )));
3222 }
3223
3224 let point_asts = points
3225 .iter()
3226 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3227 .collect::<Result<Vec<_>, _>>()?;
3228
3229 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3230 elements: point_asts,
3231 digest: None,
3232 non_code_meta: Default::default(),
3233 })));
3234
3235 self.mutate_ast(
3236 new_ast,
3237 constraint_id,
3238 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3239 )?;
3240 Ok(())
3241 }
3242
3243 fn edit_equal_length_constraint(
3245 &mut self,
3246 new_ast: &mut ast::Node<ast::Program>,
3247 constraint_id: ObjectId,
3248 lines: Vec<ObjectId>,
3249 ) -> Result<(), KclError> {
3250 if lines.len() < 2 {
3251 return Err(KclError::refactor(format!(
3252 "Lines equal length constraint must have at least 2 lines, got {}",
3253 lines.len()
3254 )));
3255 }
3256
3257 let line_asts = lines
3258 .iter()
3259 .map(|line_id| {
3260 let line_object = self
3261 .scene_graph
3262 .objects
3263 .get(line_id.0)
3264 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3265 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3266 let kind = line_object.kind.human_friendly_kind_with_article();
3267 return Err(KclError::refactor(format!(
3268 "This constraint only works on Segments, but you selected {kind}"
3269 )));
3270 };
3271 let Segment::Line(_) = line_segment else {
3272 let kind = line_segment.human_friendly_kind_with_article();
3273 return Err(KclError::refactor(format!(
3274 "Only lines can be made equal length, but you selected {kind}"
3275 )));
3276 };
3277
3278 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3279 })
3280 .collect::<Result<Vec<_>, _>>()?;
3281
3282 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3283 elements: line_asts,
3284 digest: None,
3285 non_code_meta: Default::default(),
3286 })));
3287
3288 self.mutate_ast(
3289 new_ast,
3290 constraint_id,
3291 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3292 )?;
3293 Ok(())
3294 }
3295
3296 fn edit_parallel_constraint(
3298 &mut self,
3299 new_ast: &mut ast::Node<ast::Program>,
3300 constraint_id: ObjectId,
3301 lines: Vec<ObjectId>,
3302 ) -> Result<(), KclError> {
3303 if lines.len() < 2 {
3304 return Err(KclError::refactor(format!(
3305 "Parallel constraint must have at least 2 lines, got {}",
3306 lines.len()
3307 )));
3308 }
3309
3310 let line_asts = lines
3311 .iter()
3312 .map(|line_id| {
3313 let line_object = self
3314 .scene_graph
3315 .objects
3316 .get(line_id.0)
3317 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3318 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3319 let kind = line_object.kind.human_friendly_kind_with_article();
3320 return Err(KclError::refactor(format!(
3321 "This constraint only works on Segments, but you selected {kind}"
3322 )));
3323 };
3324 let Segment::Line(_) = line_segment else {
3325 let kind = line_segment.human_friendly_kind_with_article();
3326 return Err(KclError::refactor(format!(
3327 "Only lines can be made parallel, but you selected {kind}"
3328 )));
3329 };
3330
3331 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3332 })
3333 .collect::<Result<Vec<_>, _>>()?;
3334
3335 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3336 elements: line_asts,
3337 digest: None,
3338 non_code_meta: Default::default(),
3339 })));
3340
3341 self.mutate_ast(
3342 new_ast,
3343 constraint_id,
3344 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3345 )?;
3346 Ok(())
3347 }
3348
3349 fn edit_equal_radius_constraint(
3351 &mut self,
3352 new_ast: &mut ast::Node<ast::Program>,
3353 constraint_id: ObjectId,
3354 input: Vec<ObjectId>,
3355 ) -> Result<(), KclError> {
3356 if input.len() < 2 {
3357 return Err(KclError::refactor(format!(
3358 "equalRadius constraint must have at least 2 segments, got {}",
3359 input.len()
3360 )));
3361 }
3362
3363 let input_asts = input
3364 .iter()
3365 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3366 .collect::<Result<Vec<_>, _>>()?;
3367
3368 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3369 elements: input_asts,
3370 digest: None,
3371 non_code_meta: Default::default(),
3372 })));
3373
3374 self.mutate_ast(
3375 new_ast,
3376 constraint_id,
3377 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3378 )?;
3379 Ok(())
3380 }
3381
3382 async fn execute_after_edit(
3383 &mut self,
3384 ctx: &ExecutorContext,
3385 sketch: ObjectId,
3386 sketch_block_ref: AstNodeRef,
3387 new_ast: &mut ast::Node<ast::Program>,
3388 options: ExecuteAfterEditOptions,
3389 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3390 let ExecuteAfterEditOptions {
3391 segment_ids_edited,
3392 edit_kind,
3393 commit_solved_initial_guesses,
3394 } = options;
3395
3396 let new_source = source_from_ast(new_ast);
3398 let new_program = parse_frontend_mutation_source(
3400 &new_source,
3401 "Error parsing KCL source after editing",
3402 "No AST produced after editing",
3403 )?;
3404
3405 let is_delete = edit_kind.is_delete();
3407 let truncated_program = {
3408 let mut truncated_program = new_program.clone();
3409 only_sketch_block(
3410 &mut truncated_program.ast,
3411 &sketch_block_ref,
3412 edit_kind.to_change_kind(),
3413 )
3414 .map_err(KclErrorWithOutputs::no_outputs)?;
3415 truncated_program
3416 };
3417
3418 let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3420 let mock_config = MockConfig {
3421 sketch_block_id: Some(sketch),
3422 freedom_analysis: is_delete,
3423 segment_ids_edited: segment_ids_edited.clone(),
3424 drag_anchors,
3425 ..Default::default()
3426 };
3427 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3428
3429 self.program = new_program;
3431
3432 let outcome = self.update_state_after_exec(outcome, is_delete);
3434
3435 let src_delta = if commit_solved_initial_guesses {
3436 self.commit_var_solutions_to_program(&outcome, "editing")?
3437 } else {
3438 SourceDelta { text: new_source }
3439 };
3440 let scene_graph_delta = SceneGraphDelta {
3441 new_graph: self.scene_graph_for_ui(),
3442 invalidates_ids: is_delete,
3443 new_objects: Vec::new(),
3444 exec_outcome: outcome,
3445 };
3446 Ok((src_delta, scene_graph_delta))
3447 }
3448
3449 async fn execute_after_delete_sketch(
3450 &mut self,
3451 ctx: &ExecutorContext,
3452 new_ast: &mut ast::Node<ast::Program>,
3453 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3454 let new_source = source_from_ast(new_ast);
3456 let new_program = parse_frontend_mutation_source(
3458 &new_source,
3459 "Error parsing KCL source after editing",
3460 "No AST produced after editing",
3461 )?;
3462
3463 self.program = new_program.clone();
3465
3466 let outcome = ctx.run_with_caching(new_program).await?;
3472 let freedom_analysis_ran = true;
3473
3474 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3475
3476 let src_delta = SourceDelta { text: new_source };
3477 let scene_graph_delta = SceneGraphDelta {
3478 new_graph: self.scene_graph_for_ui(),
3479 invalidates_ids: true,
3480 new_objects: Vec::new(),
3481 exec_outcome: outcome,
3482 };
3483 Ok((src_delta, scene_graph_delta))
3484 }
3485
3486 fn point_id_to_ast_reference(
3491 &self,
3492 point_id: ObjectId,
3493 new_ast: &mut ast::Node<ast::Program>,
3494 ) -> Result<ast::Expr, KclError> {
3495 let point_object = self
3496 .scene_graph
3497 .objects
3498 .get(point_id.0)
3499 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3500 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3501 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3502 };
3503 let Segment::Point(point) = point_segment else {
3504 return Err(KclError::refactor(format!(
3505 "Only points are currently supported: {point_object:?}"
3506 )));
3507 };
3508
3509 if let Some(owner_id) = point.owner {
3510 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3511 KclError::refactor(format!(
3512 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3513 ))
3514 })?;
3515 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3516 return Err(KclError::refactor(format!(
3517 "Owner of point is not a segment, but found {}",
3518 owner_object.kind.human_friendly_kind_with_article()
3519 )));
3520 };
3521
3522 match owner_segment {
3523 Segment::Line(line) => {
3524 let property = if line.start == point_id {
3525 LINE_PROPERTY_START
3526 } else if line.end == point_id {
3527 LINE_PROPERTY_END
3528 } else {
3529 return Err(KclError::refactor(format!(
3530 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3531 )));
3532 };
3533 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3534 }
3535 Segment::Arc(arc) => {
3536 let property = if arc.start == point_id {
3537 ARC_PROPERTY_START
3538 } else if arc.end == point_id {
3539 ARC_PROPERTY_END
3540 } else if arc.center == point_id {
3541 ARC_PROPERTY_CENTER
3542 } else {
3543 return Err(KclError::refactor(format!(
3544 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3545 )));
3546 };
3547 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3548 }
3549 Segment::Circle(circle) => {
3550 let property = if circle.start == point_id {
3551 CIRCLE_PROPERTY_START
3552 } else if circle.center == point_id {
3553 CIRCLE_PROPERTY_CENTER
3554 } else {
3555 return Err(KclError::refactor(format!(
3556 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3557 )));
3558 };
3559 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3560 }
3561 Segment::ControlPointSpline(spline) => {
3562 let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3563 return Err(KclError::refactor(format!(
3564 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3565 )));
3566 };
3567 let owner_expr =
3568 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3569 let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3570 Ok(create_index_expression(controls_expr, index))
3571 }
3572 _ => Err(KclError::refactor(format!(
3573 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3574 ))),
3575 }
3576 } else {
3577 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3579 }
3580 }
3581
3582 fn line_id_to_ast_reference(
3583 &self,
3584 line_id: ObjectId,
3585 new_ast: &mut ast::Node<ast::Program>,
3586 ) -> Result<ast::Expr, KclError> {
3587 let line_object = self
3588 .scene_graph
3589 .objects
3590 .get(line_id.0)
3591 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3592 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3593 return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3594 };
3595 let Segment::Line(line) = line_segment else {
3596 return Err(KclError::refactor(format!(
3597 "Only lines are currently supported: {line_object:?}"
3598 )));
3599 };
3600
3601 if let Some(owner_id) = line.owner {
3602 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3603 KclError::refactor(format!(
3604 "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3605 ))
3606 })?;
3607 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3608 return Err(KclError::refactor(format!(
3609 "Owner of line is not a segment, but found {}",
3610 owner_object.kind.human_friendly_kind_with_article()
3611 )));
3612 };
3613
3614 match owner_segment {
3615 Segment::ControlPointSpline(spline) => {
3616 let Some(index) = spline
3617 .controls
3618 .windows(2)
3619 .position(|window| window[0] == line.start && window[1] == line.end)
3620 else {
3621 return Err(KclError::refactor(format!(
3622 "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3623 )));
3624 };
3625 let owner_expr =
3626 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3627 let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3628 Ok(create_index_expression(edges_expr, index))
3629 }
3630 _ => Err(KclError::refactor(format!(
3631 "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3632 ))),
3633 }
3634 } else {
3635 get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3636 }
3637 }
3638
3639 fn coincident_segment_to_ast(
3640 &self,
3641 segment: &ConstraintSegment,
3642 new_ast: &mut ast::Node<ast::Program>,
3643 ) -> Result<ast::Expr, KclError> {
3644 match segment {
3645 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3646 ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3647 }
3648 }
3649
3650 fn segment_id_to_constraint_ast_reference(
3651 &self,
3652 segment_id: ObjectId,
3653 new_ast: &mut ast::Node<ast::Program>,
3654 ) -> Result<ast::Expr, KclError> {
3655 let segment_object = self
3656 .scene_graph
3657 .objects
3658 .get(segment_id.0)
3659 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3660 let ObjectKind::Segment { segment } = &segment_object.kind else {
3661 return Err(KclError::refactor(format!(
3662 "Object is not a segment, it is {}",
3663 segment_object.kind.human_friendly_kind_with_article()
3664 )));
3665 };
3666
3667 match segment {
3668 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3669 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3670 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3671 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3672 Segment::ControlPointSpline(_) => {
3673 get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3674 }
3675 }
3676 }
3677
3678 fn axis_constraint_segment_to_ast(
3679 &self,
3680 segment: &ConstraintSegment,
3681 new_ast: &mut ast::Node<ast::Program>,
3682 ) -> Result<ast::Expr, KclError> {
3683 match segment {
3684 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3685 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3686 }
3687 }
3688
3689 async fn add_coincident(
3690 &mut self,
3691 sketch: ObjectId,
3692 coincident: Coincident,
3693 new_ast: &mut ast::Node<ast::Program>,
3694 ) -> Result<AstNodeRef, KclError> {
3695 let sketch_id = sketch;
3696 for segment in &coincident.segments {
3697 let ConstraintSegment::Segment(segment_id) = segment else {
3698 continue;
3699 };
3700 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3701 continue;
3702 };
3703 if matches!(
3704 segment_object.kind,
3705 ObjectKind::Segment {
3706 segment: Segment::ControlPointSpline(_)
3707 }
3708 ) {
3709 return Err(KclError::refactor(
3710 "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3711 .to_owned(),
3712 ));
3713 }
3714 }
3715 let segment_asts = coincident
3716 .segments
3717 .iter()
3718 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3719 .collect::<Result<Vec<_>, _>>()?;
3720 if segment_asts.len() < 2 {
3721 return Err(KclError::refactor(format!(
3722 "Coincident constraint must have at least 2 inputs, got {}",
3723 segment_asts.len()
3724 )));
3725 }
3726
3727 let coincident_ast = create_coincident_ast(segment_asts);
3729
3730 let (sketch_block_ref, _) = self.mutate_ast(
3732 new_ast,
3733 sketch_id,
3734 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3735 )?;
3736 Ok(sketch_block_ref)
3737 }
3738
3739 async fn add_distance(
3740 &mut self,
3741 sketch: ObjectId,
3742 distance: Distance,
3743 new_ast: &mut ast::Node<ast::Program>,
3744 ) -> Result<AstNodeRef, KclError> {
3745 self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3746 }
3747
3748 fn distance_constraint_ast_parts(
3749 &self,
3750 function_name: &str,
3751 distance: &Distance,
3752 new_ast: &mut ast::Node<ast::Program>,
3753 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3754 let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3755 [pt0, pt1] => [
3756 self.coincident_segment_to_ast(pt0, new_ast)?,
3757 self.coincident_segment_to_ast(pt1, new_ast)?,
3758 ],
3759 _ => {
3760 return Err(KclError::refactor(format!(
3761 "Distance constraint must have exactly 2 segments, got {}",
3762 distance.segments.len()
3763 )));
3764 }
3765 };
3766
3767 let arguments = match &distance.label_position {
3768 Some(label_position) => vec![ast::LabeledArg {
3769 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3770 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3771 }],
3772 None => Default::default(),
3773 };
3774
3775 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3776 callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3777 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3778 ast::ArrayExpression {
3779 elements: vec![segment0_ast, segment1_ast],
3780 digest: None,
3781 non_code_meta: Default::default(),
3782 },
3783 )))),
3784 arguments,
3785 digest: None,
3786 non_code_meta: Default::default(),
3787 })));
3788 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3789 value: ast::LiteralValue::Number {
3790 value: distance.distance.value,
3791 suffix: distance.distance.units,
3792 },
3793 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3794 KclError::refactor(format!(
3795 "Could not format numeric suffix: {:?}",
3796 distance.distance.units
3797 ))
3798 })?,
3799 digest: None,
3800 })));
3801
3802 Ok((call, value))
3803 }
3804
3805 fn add_distance_constraint(
3806 &mut self,
3807 sketch: ObjectId,
3808 function_name: &str,
3809 distance: Distance,
3810 new_ast: &mut ast::Node<ast::Program>,
3811 ) -> Result<AstNodeRef, KclError> {
3812 let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3813 let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3814 left: call,
3815 operator: ast::BinaryOperator::Eq,
3816 right: value,
3817 digest: None,
3818 })));
3819
3820 let (sketch_block_ref, _) = self.mutate_ast(
3821 new_ast,
3822 sketch,
3823 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3824 )?;
3825 Ok(sketch_block_ref)
3826 }
3827
3828 async fn add_angle(
3829 &mut self,
3830 sketch: ObjectId,
3831 angle: Angle,
3832 new_ast: &mut ast::Node<ast::Program>,
3833 ) -> Result<AstNodeRef, KclError> {
3834 let sketch_id = sketch;
3835 let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3836 let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3837 left: angle_call_ast,
3838 operator: ast::BinaryOperator::Eq,
3839 right: angle_value_ast,
3840 digest: None,
3841 })));
3842
3843 let (sketch_block_ref, _) = self.mutate_ast(
3845 new_ast,
3846 sketch_id,
3847 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3848 )?;
3849 Ok(sketch_block_ref)
3850 }
3851
3852 fn angle_constraint_ast_parts(
3853 &self,
3854 angle: &Angle,
3855 new_ast: &mut ast::Node<ast::Program>,
3856 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3857 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3858 return Err(KclError::refactor(format!(
3859 "Angle constraint must have exactly 2 lines, got {}",
3860 angle.lines.len()
3861 )));
3862 };
3863
3864 let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3865 let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3866 let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3867 elements: vec![l0_ast, l1_ast],
3868 digest: None,
3869 non_code_meta: Default::default(),
3870 })));
3871
3872 if angle.inverse == Some(true) && angle.sector.is_none() {
3873 return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3874 }
3875
3876 let uses_angle_dimension = angle.sector.is_some();
3877 let mut arguments = if uses_angle_dimension {
3878 vec![ast::LabeledArg {
3879 label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3880 arg: lines_ast.clone(),
3881 }]
3882 } else {
3883 Default::default()
3884 };
3885
3886 if let Some(sector) = angle.sector {
3887 arguments.push(ast::LabeledArg {
3888 label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3889 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3890 value: ast::LiteralValue::Number {
3891 value: f64::from(sector),
3892 suffix: NumericSuffix::None,
3893 },
3894 raw: sector.to_string(),
3895 digest: None,
3896 }))),
3897 });
3898 }
3899
3900 if angle.inverse == Some(true) {
3901 arguments.push(ast::LabeledArg {
3902 label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3903 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3904 value: ast::LiteralValue::Bool(true),
3905 raw: true.to_string(),
3906 digest: None,
3907 }))),
3908 });
3909 }
3910
3911 if let Some(label_position) = &angle.label_position {
3912 arguments.push(ast::LabeledArg {
3913 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3914 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3915 });
3916 }
3917
3918 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3919 callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3920 ANGLE_DIMENSION_FN
3921 } else {
3922 ANGLE_FN
3923 })),
3924 unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3925 arguments,
3926 digest: None,
3927 non_code_meta: Default::default(),
3928 })));
3929 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3930 value: ast::LiteralValue::Number {
3931 value: angle.angle.value,
3932 suffix: angle.angle.units,
3933 },
3934 raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3935 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3936 digest: None,
3937 })));
3938
3939 Ok((call, value))
3940 }
3941
3942 async fn add_tangent(
3943 &mut self,
3944 sketch: ObjectId,
3945 tangent: Tangent,
3946 new_ast: &mut ast::Node<ast::Program>,
3947 ) -> Result<AstNodeRef, KclError> {
3948 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3949 return Err(KclError::refactor(format!(
3950 "Tangent constraint must have exactly 2 segments, got {}",
3951 tangent.input.len()
3952 )));
3953 };
3954 let sketch_id = sketch;
3955
3956 let seg0_object = self
3957 .scene_graph
3958 .objects
3959 .get(seg0_id.0)
3960 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3961 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3962 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3963 };
3964 let seg0_ast = match seg0_segment {
3965 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3966 self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3967 }
3968 _ => {
3969 return Err(KclError::refactor(format!(
3970 "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3971 )));
3972 }
3973 };
3974
3975 let seg1_object = self
3976 .scene_graph
3977 .objects
3978 .get(seg1_id.0)
3979 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3980 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3981 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3982 };
3983 let seg1_ast = match seg1_segment {
3984 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3985 self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3986 }
3987 _ => {
3988 return Err(KclError::refactor(format!(
3989 "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3990 )));
3991 }
3992 };
3993
3994 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3995 let (sketch_block_ref, _) = self.mutate_ast(
3996 new_ast,
3997 sketch_id,
3998 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3999 )?;
4000 Ok(sketch_block_ref)
4001 }
4002
4003 async fn add_symmetric(
4004 &mut self,
4005 sketch: ObjectId,
4006 symmetric: Symmetric,
4007 new_ast: &mut ast::Node<ast::Program>,
4008 ) -> Result<AstNodeRef, KclError> {
4009 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4010 return Err(KclError::refactor(format!(
4011 "Symmetric constraint must have exactly 2 inputs, got {}",
4012 symmetric.input.len()
4013 )));
4014 };
4015 let sketch_id = sketch;
4016
4017 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4018 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4019 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4020
4021 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4022 let (sketch_block_ref, _) = self.mutate_ast(
4023 new_ast,
4024 sketch_id,
4025 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4026 )?;
4027 Ok(sketch_block_ref)
4028 }
4029
4030 async fn add_midpoint(
4031 &mut self,
4032 sketch: ObjectId,
4033 midpoint: Midpoint,
4034 new_ast: &mut ast::Node<ast::Program>,
4035 ) -> Result<AstNodeRef, KclError> {
4036 let sketch_id = sketch;
4037 let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4038
4039 let segment_object = self
4040 .scene_graph
4041 .objects
4042 .get(midpoint.segment.0)
4043 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4044 let ObjectKind::Segment {
4045 segment: midpoint_segment,
4046 } = &segment_object.kind
4047 else {
4048 return Err(KclError::refactor(format!(
4049 "Object must be a segment, but it was {}",
4050 segment_object.kind.human_friendly_kind_with_article()
4051 )));
4052 };
4053 let segment_ast = match midpoint_segment {
4054 Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4055 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4056 _ => {
4057 return Err(KclError::refactor(format!(
4058 "Midpoint target must be a line or arc segment but it was {}",
4059 midpoint_segment.human_friendly_kind_with_article()
4060 )));
4061 }
4062 };
4063
4064 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4065 let (sketch_block_ref, _) = self.mutate_ast(
4066 new_ast,
4067 sketch_id,
4068 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4069 )?;
4070 Ok(sketch_block_ref)
4071 }
4072
4073 async fn add_equal_radius(
4074 &mut self,
4075 sketch: ObjectId,
4076 equal_radius: EqualRadius,
4077 new_ast: &mut ast::Node<ast::Program>,
4078 ) -> Result<AstNodeRef, KclError> {
4079 if equal_radius.input.len() < 2 {
4080 return Err(KclError::refactor(format!(
4081 "equalRadius constraint must have at least 2 segments, got {}",
4082 equal_radius.input.len()
4083 )));
4084 }
4085
4086 let sketch_id = sketch;
4087 let input_asts = equal_radius
4088 .input
4089 .iter()
4090 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4091 .collect::<Result<Vec<_>, _>>()?;
4092
4093 let equal_radius_ast = create_equal_radius_ast(input_asts);
4094 let (sketch_block_ref, _) = self.mutate_ast(
4095 new_ast,
4096 sketch_id,
4097 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4098 )?;
4099 Ok(sketch_block_ref)
4100 }
4101
4102 async fn add_radius(
4103 &mut self,
4104 sketch: ObjectId,
4105 radius: Radius,
4106 new_ast: &mut ast::Node<ast::Program>,
4107 ) -> Result<AstNodeRef, KclError> {
4108 let params = ArcSizeConstraintParams {
4109 points: vec![radius.arc],
4110 function_name: RADIUS_FN,
4111 value: radius.radius.value,
4112 units: radius.radius.units,
4113 label_position: radius.label_position,
4114 constraint_type_name: "Radius",
4115 };
4116 self.add_arc_size_constraint(sketch, params, new_ast).await
4117 }
4118
4119 async fn add_diameter(
4120 &mut self,
4121 sketch: ObjectId,
4122 diameter: Diameter,
4123 new_ast: &mut ast::Node<ast::Program>,
4124 ) -> Result<AstNodeRef, KclError> {
4125 let params = ArcSizeConstraintParams {
4126 points: vec![diameter.arc],
4127 function_name: DIAMETER_FN,
4128 value: diameter.diameter.value,
4129 units: diameter.diameter.units,
4130 label_position: diameter.label_position,
4131 constraint_type_name: "Diameter",
4132 };
4133 self.add_arc_size_constraint(sketch, params, new_ast).await
4134 }
4135
4136 async fn add_fixed_constraints(
4137 &mut self,
4138 sketch: ObjectId,
4139 points: Vec<FixedPoint>,
4140 new_ast: &mut ast::Node<ast::Program>,
4141 ) -> Result<AstNodeRef, KclError> {
4142 let mut sketch_block_ref = None;
4143
4144 for fixed_point in points {
4145 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4146 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4147 .map_err(|err| KclError::refactor(err.to_string()))?;
4148
4149 let (sketch_ref, _) = self.mutate_ast(
4150 new_ast,
4151 sketch,
4152 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4153 )?;
4154 sketch_block_ref = Some(sketch_ref);
4155 }
4156
4157 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4158 }
4159
4160 async fn add_arc_size_constraint(
4161 &mut self,
4162 sketch: ObjectId,
4163 params: ArcSizeConstraintParams,
4164 new_ast: &mut ast::Node<ast::Program>,
4165 ) -> Result<AstNodeRef, KclError> {
4166 let sketch_id = sketch;
4167
4168 if params.points.len() != 1 {
4170 return Err(KclError::refactor(format!(
4171 "{} constraint must have exactly 1 argument (an arc segment), got {}",
4172 params.constraint_type_name,
4173 params.points.len()
4174 )));
4175 }
4176
4177 let arc_id = params.points[0];
4178 let arc_object = self
4179 .scene_graph
4180 .objects
4181 .get(arc_id.0)
4182 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4183 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4184 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4185 };
4186 let ref_type = match arc_segment {
4187 Segment::Arc(_) => ARC_VARIABLE,
4188 Segment::Circle(_) => CIRCLE_VARIABLE,
4189 _ => {
4190 return Err(KclError::refactor(format!(
4191 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4192 params.constraint_type_name
4193 )));
4194 }
4195 };
4196 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4198 let arguments = match ¶ms.label_position {
4199 Some(label_position) => vec![ast::LabeledArg {
4200 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4201 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4202 }],
4203 None => Default::default(),
4204 };
4205
4206 let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4208 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4209 unlabeled: Some(arc_ast),
4210 arguments,
4211 digest: None,
4212 non_code_meta: Default::default(),
4213 })));
4214 let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4215 left: call_ast,
4216 operator: ast::BinaryOperator::Eq,
4217 right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4218 value: ast::LiteralValue::Number {
4219 value: params.value,
4220 suffix: params.units,
4221 },
4222 raw: format_number_literal(params.value, params.units, None)
4223 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4224 digest: None,
4225 }))),
4226 digest: None,
4227 })));
4228
4229 let (sketch_block_ref, _) = self.mutate_ast(
4231 new_ast,
4232 sketch_id,
4233 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4234 )?;
4235 Ok(sketch_block_ref)
4236 }
4237
4238 async fn add_horizontal_distance(
4239 &mut self,
4240 sketch: ObjectId,
4241 distance: Distance,
4242 new_ast: &mut ast::Node<ast::Program>,
4243 ) -> Result<AstNodeRef, KclError> {
4244 self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4245 }
4246
4247 async fn add_vertical_distance(
4248 &mut self,
4249 sketch: ObjectId,
4250 distance: Distance,
4251 new_ast: &mut ast::Node<ast::Program>,
4252 ) -> Result<AstNodeRef, KclError> {
4253 self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4254 }
4255
4256 async fn add_horizontal(
4257 &mut self,
4258 sketch: ObjectId,
4259 horizontal: Horizontal,
4260 new_ast: &mut ast::Node<ast::Program>,
4261 ) -> Result<AstNodeRef, KclError> {
4262 let sketch_id = sketch;
4263
4264 let first_arg_ast = match horizontal {
4266 Horizontal::Line { line } => {
4267 let line_object = self
4268 .scene_graph
4269 .objects
4270 .get(line.0)
4271 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4272 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4273 let kind = line_object.kind.human_friendly_kind_with_article();
4274 return Err(KclError::refactor(format!(
4275 "This constraint only works on Segments, but you selected {kind}"
4276 )));
4277 };
4278 let Segment::Line(_) = line_segment else {
4279 return Err(KclError::refactor(format!(
4280 "Only lines can be made horizontal, but you selected {}",
4281 line_segment.human_friendly_kind_with_article(),
4282 )));
4283 };
4284 self.line_id_to_ast_reference(line, new_ast)?
4285 }
4286 Horizontal::Points { points } => {
4287 let point_asts = points
4288 .iter()
4289 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4290 .collect::<Result<Vec<_>, _>>()?;
4291 ast::ArrayExpression::new(point_asts).into()
4292 }
4293 };
4294 let horizontal_ast = create_horizontal_ast(first_arg_ast);
4296
4297 let (sketch_block_ref, _) = self.mutate_ast(
4299 new_ast,
4300 sketch_id,
4301 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4302 )?;
4303 Ok(sketch_block_ref)
4304 }
4305
4306 async fn add_lines_equal_length(
4307 &mut self,
4308 sketch: ObjectId,
4309 lines_equal_length: LinesEqualLength,
4310 new_ast: &mut ast::Node<ast::Program>,
4311 ) -> Result<AstNodeRef, KclError> {
4312 if lines_equal_length.lines.len() < 2 {
4313 return Err(KclError::refactor(format!(
4314 "Lines equal length constraint must have at least 2 lines, got {}",
4315 lines_equal_length.lines.len()
4316 )));
4317 };
4318
4319 let sketch_id = sketch;
4320
4321 let line_asts = lines_equal_length
4323 .lines
4324 .iter()
4325 .map(|line_id| {
4326 let line_object = self
4327 .scene_graph
4328 .objects
4329 .get(line_id.0)
4330 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4331 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4332 let kind = line_object.kind.human_friendly_kind_with_article();
4333 return Err(KclError::refactor(format!(
4334 "This constraint only works on Segments, but you selected {kind}"
4335 )));
4336 };
4337 let Segment::Line(_) = line_segment else {
4338 let kind = line_segment.human_friendly_kind_with_article();
4339 return Err(KclError::refactor(format!(
4340 "Only lines can be made equal length, but you selected {kind}"
4341 )));
4342 };
4343
4344 self.line_id_to_ast_reference(*line_id, new_ast)
4345 })
4346 .collect::<Result<Vec<_>, _>>()?;
4347
4348 let equal_length_ast = create_equal_length_ast(line_asts);
4350
4351 let (sketch_block_ref, _) = self.mutate_ast(
4353 new_ast,
4354 sketch_id,
4355 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4356 )?;
4357 Ok(sketch_block_ref)
4358 }
4359
4360 fn equal_radius_segment_id_to_ast_reference(
4361 &mut self,
4362 segment_id: ObjectId,
4363 new_ast: &mut ast::Node<ast::Program>,
4364 ) -> Result<ast::Expr, KclError> {
4365 let segment_object = self
4366 .scene_graph
4367 .objects
4368 .get(segment_id.0)
4369 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4370 let ObjectKind::Segment { segment } = &segment_object.kind else {
4371 return Err(KclError::refactor(format!(
4372 "Object is not a segment, it was {}",
4373 segment_object.kind.human_friendly_kind_with_article()
4374 )));
4375 };
4376
4377 let ref_type = match segment {
4378 Segment::Arc(_) => ARC_VARIABLE,
4379 Segment::Circle(_) => CIRCLE_VARIABLE,
4380 _ => {
4381 return Err(KclError::refactor(format!(
4382 "equalRadius supports only arc/circle segments, got {}",
4383 segment.human_friendly_kind_with_article()
4384 )));
4385 }
4386 };
4387
4388 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4389 }
4390
4391 fn symmetric_input_id_to_ast_reference(
4392 &mut self,
4393 segment_id: ObjectId,
4394 new_ast: &mut ast::Node<ast::Program>,
4395 ) -> Result<ast::Expr, KclError> {
4396 let segment_object = self
4397 .scene_graph
4398 .objects
4399 .get(segment_id.0)
4400 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4401 let ObjectKind::Segment { segment } = &segment_object.kind else {
4402 return Err(KclError::refactor(format!(
4403 "Object is not a segment, it was {}",
4404 segment_object.kind.human_friendly_kind_with_article()
4405 )));
4406 };
4407
4408 match segment {
4409 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4410 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4411 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4412 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4413 Segment::ControlPointSpline(_) => Err(KclError::refactor(
4414 "Symmetric does not yet support control point splines".to_owned(),
4415 )),
4416 }
4417 }
4418
4419 fn symmetric_axis_id_to_ast_reference(
4420 &mut self,
4421 segment_id: ObjectId,
4422 new_ast: &mut ast::Node<ast::Program>,
4423 ) -> Result<ast::Expr, KclError> {
4424 let segment_object = self
4425 .scene_graph
4426 .objects
4427 .get(segment_id.0)
4428 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4429 let ObjectKind::Segment { segment } = &segment_object.kind else {
4430 return Err(KclError::refactor(format!(
4431 "Object is not a segment, it was {}",
4432 segment_object.kind.human_friendly_kind_with_article()
4433 )));
4434 };
4435 match segment {
4436 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4437 _ => Err(KclError::refactor(format!(
4438 "Symmetric axis must be a line, got {}",
4439 segment.human_friendly_kind_with_article()
4440 ))),
4441 }
4442 }
4443
4444 async fn add_parallel(
4445 &mut self,
4446 sketch: ObjectId,
4447 parallel: Parallel,
4448 new_ast: &mut ast::Node<ast::Program>,
4449 ) -> Result<AstNodeRef, KclError> {
4450 if parallel.lines.len() < 2 {
4451 return Err(KclError::refactor(format!(
4452 "Parallel constraint must have at least 2 lines, got {}",
4453 parallel.lines.len()
4454 )));
4455 };
4456
4457 let sketch_id = sketch;
4458
4459 let line_asts = parallel
4460 .lines
4461 .iter()
4462 .map(|line_id| {
4463 let line_object = self
4464 .scene_graph
4465 .objects
4466 .get(line_id.0)
4467 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4468 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4469 let kind = line_object.kind.human_friendly_kind_with_article();
4470 return Err(KclError::refactor(format!(
4471 "This constraint only works on Segments, but you selected {kind}"
4472 )));
4473 };
4474 let Segment::Line(_) = line_segment else {
4475 let kind = line_segment.human_friendly_kind_with_article();
4476 return Err(KclError::refactor(format!(
4477 "Only lines can be made parallel, but you selected {kind}"
4478 )));
4479 };
4480
4481 self.line_id_to_ast_reference(*line_id, new_ast)
4482 })
4483 .collect::<Result<Vec<_>, _>>()?;
4484
4485 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4486 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4487 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4488 ast::ArrayExpression {
4489 elements: line_asts,
4490 digest: None,
4491 non_code_meta: Default::default(),
4492 },
4493 )))),
4494 arguments: Default::default(),
4495 digest: None,
4496 non_code_meta: Default::default(),
4497 })));
4498
4499 let (sketch_block_ref, _) = self.mutate_ast(
4500 new_ast,
4501 sketch_id,
4502 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4503 )?;
4504 Ok(sketch_block_ref)
4505 }
4506
4507 async fn add_perpendicular(
4508 &mut self,
4509 sketch: ObjectId,
4510 perpendicular: Perpendicular,
4511 new_ast: &mut ast::Node<ast::Program>,
4512 ) -> Result<AstNodeRef, KclError> {
4513 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4514 .await
4515 }
4516
4517 async fn add_lines_at_angle_constraint(
4518 &mut self,
4519 sketch: ObjectId,
4520 angle_kind: LinesAtAngleKind,
4521 lines: Vec<ObjectId>,
4522 new_ast: &mut ast::Node<ast::Program>,
4523 ) -> Result<AstNodeRef, KclError> {
4524 let &[line0_id, line1_id] = lines.as_slice() else {
4525 return Err(KclError::refactor(format!(
4526 "{} constraint must have exactly 2 lines, got {}",
4527 angle_kind.to_function_name(),
4528 lines.len()
4529 )));
4530 };
4531
4532 let sketch_id = sketch;
4533
4534 let line0_object = self
4536 .scene_graph
4537 .objects
4538 .get(line0_id.0)
4539 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4540 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4541 let kind = line0_object.kind.human_friendly_kind_with_article();
4542 return Err(KclError::refactor(format!(
4543 "This constraint only works on Segments, but you selected {kind}"
4544 )));
4545 };
4546 let Segment::Line(_) = line0_segment else {
4547 return Err(KclError::refactor(format!(
4548 "Only lines can be made {}, but you selected {}",
4549 angle_kind.to_function_name(),
4550 line0_segment.human_friendly_kind_with_article(),
4551 )));
4552 };
4553 let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4554
4555 let line1_object = self
4556 .scene_graph
4557 .objects
4558 .get(line1_id.0)
4559 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4560 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4561 let kind = line1_object.kind.human_friendly_kind_with_article();
4562 return Err(KclError::refactor(format!(
4563 "This constraint only works on Segments, but you selected {kind}"
4564 )));
4565 };
4566 let Segment::Line(_) = line1_segment else {
4567 return Err(KclError::refactor(format!(
4568 "Only lines can be made {}, but you selected {}",
4569 angle_kind.to_function_name(),
4570 line1_segment.human_friendly_kind_with_article(),
4571 )));
4572 };
4573 let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4574
4575 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4577 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4578 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4579 ast::ArrayExpression {
4580 elements: vec![line0_ast, line1_ast],
4581 digest: None,
4582 non_code_meta: Default::default(),
4583 },
4584 )))),
4585 arguments: Default::default(),
4586 digest: None,
4587 non_code_meta: Default::default(),
4588 })));
4589
4590 let (sketch_block_ref, _) = self.mutate_ast(
4592 new_ast,
4593 sketch_id,
4594 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4595 )?;
4596 Ok(sketch_block_ref)
4597 }
4598
4599 async fn add_vertical(
4600 &mut self,
4601 sketch: ObjectId,
4602 vertical: Vertical,
4603 new_ast: &mut ast::Node<ast::Program>,
4604 ) -> Result<AstNodeRef, KclError> {
4605 let sketch_id = sketch;
4606
4607 let first_arg_ast = match vertical {
4608 Vertical::Line { line } => {
4609 let line_object = self
4611 .scene_graph
4612 .objects
4613 .get(line.0)
4614 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4615 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4616 let kind = line_object.kind.human_friendly_kind_with_article();
4617 return Err(KclError::refactor(format!(
4618 "This constraint only works on Segments, but you selected {kind}"
4619 )));
4620 };
4621 let Segment::Line(_) = line_segment else {
4622 return Err(KclError::refactor(format!(
4623 "Only lines can be made vertical, but you selected {}",
4624 line_segment.human_friendly_kind_with_article()
4625 )));
4626 };
4627 self.line_id_to_ast_reference(line, new_ast)?
4628 }
4629 Vertical::Points { points } => {
4630 let point_asts = points
4631 .iter()
4632 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4633 .collect::<Result<Vec<_>, _>>()?;
4634 ast::ArrayExpression::new(point_asts).into()
4635 }
4636 };
4637 let vertical_ast = create_vertical_ast(first_arg_ast);
4639
4640 let (sketch_block_ref, _) = self.mutate_ast(
4642 new_ast,
4643 sketch_id,
4644 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4645 )?;
4646 Ok(sketch_block_ref)
4647 }
4648
4649 async fn execute_after_add_constraint(
4650 &mut self,
4651 ctx: &ExecutorContext,
4652 sketch_id: ObjectId,
4653 sketch_block_ref: AstNodeRef,
4654 new_ast: &mut ast::Node<ast::Program>,
4655 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4656 let new_source = source_from_ast(new_ast);
4658 let new_program = parse_frontend_mutation_source(
4660 &new_source,
4661 "Error parsing KCL source after adding constraint",
4662 "No AST produced after adding constraint",
4663 )?;
4664 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4665 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4666 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4667 )))
4668 })?;
4669
4670 let mut truncated_program = new_program.clone();
4673 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4674 .map_err(KclErrorWithOutputs::no_outputs)?;
4675
4676 let outcome = ctx
4678 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4679 .await?;
4680
4681 let new_object_ids = {
4682 let constraint_id = outcome
4684 .source_range_to_object
4685 .get(&constraint_node_ref.range)
4686 .copied()
4687 .ok_or_else(|| {
4688 KclErrorWithOutputs::from_error_outcome(
4689 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4690 outcome.clone(),
4691 )
4692 })?;
4693 vec![constraint_id]
4694 };
4695
4696 self.program = new_program;
4699
4700 let outcome = self.update_state_after_exec(outcome, true);
4702
4703 let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4704 let scene_graph_delta = SceneGraphDelta {
4705 new_graph: self.scene_graph_for_ui(),
4706 invalidates_ids: false,
4707 new_objects: new_object_ids,
4708 exec_outcome: outcome,
4709 };
4710 Ok((src_delta, scene_graph_delta))
4711 }
4712
4713 fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4714 let commit_failure = || {
4715 KclErrorWithOutputs::from_error_outcome(
4716 KclError::refactor(format!("Could not update KCL after {operation}.")),
4717 outcome.clone(),
4718 )
4719 };
4720
4721 let default_length_unit = self.default_length_unit();
4722 let mut settled_ast = self.program.ast.clone();
4723 let mut committed_solver_value = false;
4724 for (var_range, node_path, value) in &outcome.var_solutions {
4725 let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4726 return Err(commit_failure());
4727 };
4728 let new_value = match &lookup {
4729 Some(current_literal) => {
4730 if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4731 continue;
4732 }
4733 preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4734 }
4735 None => {
4736 Number {
4740 value: number_value_in_default_length_units(*value, default_length_unit),
4741 units: default_length_unit.into(),
4742 }
4743 }
4744 };
4745 committed_solver_value = true;
4746 let source_ref = SourceRef::Simple {
4747 range: *var_range,
4748 node_path: node_path.clone(),
4749 };
4750 mutate_ast_node_by_source_ref(
4751 &mut settled_ast,
4752 &source_ref,
4753 AstMutateCommand::EditVarInitialValue { value: new_value },
4754 )
4755 .map_err(|_| commit_failure())?;
4756 }
4757
4758 if !committed_solver_value {
4759 return Ok(SourceDelta {
4760 text: self.program.original_file_contents.clone(),
4761 });
4762 }
4763
4764 let settled_source = source_from_ast(&settled_ast);
4765 let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4766 if !errors.is_empty() {
4767 return Err(commit_failure());
4768 }
4769 let Some(settled_program) = settled_program else {
4770 return Err(commit_failure());
4771 };
4772
4773 self.program = settled_program;
4774
4775 Ok(SourceDelta { text: settled_source })
4776 }
4777
4778 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4780 if segment_ids_set.contains(&segment_id) {
4781 return true;
4782 }
4783
4784 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4785 return false;
4786 };
4787 let ObjectKind::Segment { segment } = &segment_object.kind else {
4788 return false;
4789 };
4790 let Segment::Point(point) = segment else {
4791 return false;
4792 };
4793
4794 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4795 }
4796
4797 fn remaining_constraint_segments(
4798 &self,
4799 segments: &[ConstraintSegment],
4800 segment_ids_set: &AhashIndexSet<ObjectId>,
4801 ) -> Vec<ConstraintSegment> {
4802 segments
4803 .iter()
4804 .copied()
4805 .filter(|segment| match segment {
4806 ConstraintSegment::Origin(_) => true,
4807 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4808 })
4809 .collect()
4810 }
4811
4812 fn find_referenced_constraints(
4813 &self,
4814 sketch_id: ObjectId,
4815 segment_ids_set: &AhashIndexSet<ObjectId>,
4816 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4817 let sketch_object = self
4819 .scene_graph
4820 .objects
4821 .get(sketch_id.0)
4822 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4823 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4824 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4825 };
4826 let segment_or_owner_matches = |segment_id: ObjectId| {
4827 if segment_ids_set.contains(&segment_id) {
4828 return true;
4829 }
4830 let segment_object = self.scene_graph.objects.get(segment_id.0);
4831 if let Some(obj) = segment_object
4832 && let ObjectKind::Segment { segment } = &obj.kind
4833 {
4834 match segment {
4835 Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4836 Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4837 _ => false,
4838 }
4839 } else {
4840 false
4841 }
4842 };
4843 let mut constraint_ids_set = AhashIndexSet::default();
4844 for constraint_id in &sketch.constraints {
4845 let constraint_object = self
4846 .scene_graph
4847 .objects
4848 .get(constraint_id.0)
4849 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4850 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4851 return Err(KclError::refactor(format!(
4852 "Object is not a constraint, it is {}",
4853 constraint_object.kind.human_friendly_kind_with_article()
4854 )));
4855 };
4856 let depends_on_segment = match constraint {
4857 Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4858 Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4859 Constraint::Fixed(fixed) => fixed
4860 .points
4861 .iter()
4862 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4863 Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4864 Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4865 Constraint::EqualRadius(equal_radius) => {
4866 equal_radius.input.iter().copied().any(segment_or_owner_matches)
4867 }
4868 Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4869 Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4870 Constraint::Horizontal(h) => match h {
4871 Horizontal::Line { line } => segment_or_owner_matches(*line),
4872 Horizontal::Points { points } => points.iter().any(|point| match point {
4873 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4874 ConstraintSegment::Origin(_) => false,
4875 }),
4876 },
4877 Constraint::Vertical(v) => match v {
4878 Vertical::Line { line } => segment_or_owner_matches(*line),
4879 Vertical::Points { points } => points.iter().any(|point| match point {
4880 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4881 ConstraintSegment::Origin(_) => false,
4882 }),
4883 },
4884 Constraint::LinesEqualLength(lines_equal_length) => {
4885 lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4886 }
4887 Constraint::Midpoint(midpoint) => {
4888 segment_or_owner_matches(midpoint.segment)
4889 || matches!(
4890 midpoint.point,
4891 ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4892 )
4893 }
4894 Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4895 Constraint::Perpendicular(perpendicular) => {
4896 perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4897 }
4898 Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4899 Constraint::Symmetric(symmetric) => {
4900 segment_or_owner_matches(symmetric.axis)
4901 || symmetric.input.iter().copied().any(segment_or_owner_matches)
4902 }
4903 Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4904 };
4905 if depends_on_segment {
4906 constraint_ids_set.insert(*constraint_id);
4907 }
4908 }
4909 Ok(constraint_ids_set)
4910 }
4911
4912 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4913 let mut outcome = outcome;
4914 self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4915 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4916
4917 if freedom_analysis_ran {
4918 self.point_freedom_cache.clear();
4921 for new_obj in &new_objects {
4922 if let ObjectKind::Segment {
4923 segment: crate::front::Segment::Point(point),
4924 } = &new_obj.kind
4925 {
4926 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4927 }
4928 }
4929 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4930 self.scene_graph.objects = new_objects;
4932 } else {
4933 for old_obj in &self.scene_graph.objects {
4936 if let ObjectKind::Segment {
4937 segment: crate::front::Segment::Point(point),
4938 } = &old_obj.kind
4939 {
4940 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4941 }
4942 }
4943
4944 let mut updated_objects = Vec::with_capacity(new_objects.len());
4946 for new_obj in new_objects {
4947 let mut obj = new_obj;
4948 if let ObjectKind::Segment {
4949 segment: crate::front::Segment::Point(point),
4950 } = &mut obj.kind
4951 {
4952 let new_freedom = point.freedom;
4953 match new_freedom {
4959 Freedom::Free => {
4960 match self.point_freedom_cache.get(&obj.id).copied() {
4961 Some(Freedom::Conflict) => {
4962 }
4965 Some(Freedom::Fixed) => {
4966 point.freedom = Freedom::Fixed;
4968 }
4969 Some(Freedom::Free) => {
4970 }
4972 None => {
4973 }
4975 }
4976 }
4977 Freedom::Fixed => {
4978 }
4980 Freedom::Conflict => {
4981 }
4983 }
4984 self.point_freedom_cache.insert(obj.id, point.freedom);
4986 }
4987 updated_objects.push(obj);
4988 }
4989
4990 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4991 self.scene_graph.objects = updated_objects;
4992 }
4993 outcome
4994 }
4995
4996 fn mutate_ast(
4997 &mut self,
4998 ast: &mut ast::Node<ast::Program>,
4999 object_id: ObjectId,
5000 command: AstMutateCommand,
5001 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5002 let sketch_object = self
5003 .scene_graph
5004 .objects
5005 .get(object_id.0)
5006 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5007 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5008 }
5009
5010 fn mutate_constraint_label_position(
5011 &mut self,
5012 ast: &mut ast::Node<ast::Program>,
5013 constraint_id: ObjectId,
5014 label_position: Point2d<Number>,
5015 ) -> Result<(), KclError> {
5016 let object = self
5017 .scene_graph
5018 .objects
5019 .get(constraint_id.0)
5020 .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5021 if !matches!(
5022 &object.kind,
5023 ObjectKind::Constraint {
5024 constraint: Constraint::Distance(_)
5025 | Constraint::HorizontalDistance(_)
5026 | Constraint::VerticalDistance(_)
5027 | Constraint::Radius(_)
5028 | Constraint::Diameter(_)
5029 | Constraint::Angle(_),
5030 }
5031 ) {
5032 return Err(KclError::refactor(format!(
5033 "Object does not support labelPosition: {constraint_id:?}"
5034 )));
5035 }
5036
5037 let label_position = to_ast_point2d_number(&label_position)
5038 .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5039 self.mutate_ast(
5040 ast,
5041 constraint_id,
5042 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5043 )?;
5044 Ok(())
5045 }
5046}
5047
5048fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5049 let sketch_object = scene_graph
5051 .objects
5052 .get(sketch_id.0)
5053 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5054 let ObjectKind::Sketch(_) = &sketch_object.kind else {
5055 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5056 };
5057 expect_single_node_ref(sketch_object)
5058}
5059
5060fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5061 match &object.source {
5062 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5063 range: *range,
5064 node_path: node_path.clone(),
5065 }),
5066 SourceRef::BackTrace { ranges } => {
5067 let [range] = ranges.as_slice() else {
5068 return Err(KclError::refactor(format!(
5069 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5070 ranges.len()
5071 )));
5072 };
5073 Ok(AstNodeRef {
5074 range: range.0,
5075 node_path: range.1.clone(),
5076 })
5077 }
5078 }
5079}
5080
5081fn only_sketch_block_from_range(
5084 ast: &mut ast::Node<ast::Program>,
5085 sketch_block_range: SourceRange,
5086 edit_kind: ChangeKind,
5087) -> Result<(), KclError> {
5088 let r1 = sketch_block_range;
5089 let matches_range = |r2: SourceRange| -> bool {
5090 match edit_kind {
5093 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5094 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5096 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5097 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5099 }
5100 };
5101 let mut found = false;
5102 for item in ast.body.iter_mut() {
5103 match item {
5104 ast::BodyItem::ImportStatement(_) => {}
5105 ast::BodyItem::ExpressionStatement(node) => {
5106 if matches_range(SourceRange::from(&*node))
5107 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5108 {
5109 sketch_block.is_being_edited = true;
5110 found = true;
5111 break;
5112 }
5113 }
5114 ast::BodyItem::VariableDeclaration(node) => {
5115 if matches_range(SourceRange::from(&node.declaration.init))
5116 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5117 {
5118 sketch_block.is_being_edited = true;
5119 found = true;
5120 break;
5121 }
5122 }
5123 ast::BodyItem::TypeDeclaration(_) => {}
5124 ast::BodyItem::ReturnStatement(node) => {
5125 if matches_range(SourceRange::from(&node.argument))
5126 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5127 {
5128 sketch_block.is_being_edited = true;
5129 found = true;
5130 break;
5131 }
5132 }
5133 }
5134 }
5135 if !found {
5136 return Err(KclError::refactor(format!(
5137 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5138 )));
5139 }
5140
5141 Ok(())
5142}
5143
5144fn only_sketch_block(
5145 ast: &mut ast::Node<ast::Program>,
5146 sketch_block_ref: &AstNodeRef,
5147 edit_kind: ChangeKind,
5148) -> Result<(), KclError> {
5149 let Some(target_node_path) = &sketch_block_ref.node_path else {
5150 #[cfg(target_arch = "wasm32")]
5151 web_sys::console::warn_1(
5152 &format!(
5153 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5154 sketch_block_ref
5155 )
5156 .into(),
5157 );
5158 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5159 };
5160 struct MarkSketchBlockBeingEdited<'a> {
5161 target_node_path: &'a ast::NodePath,
5162 }
5163
5164 impl Visitor for MarkSketchBlockBeingEdited<'_> {
5165 type Break = ();
5166 type Continue = ();
5167
5168 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5169 if let NodeMut::SketchBlock(sketch_block) = node
5170 && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5171 {
5172 sketch_block.is_being_edited = true;
5173 return TraversalReturn::new_break(());
5174 }
5175 TraversalReturn::new_continue(())
5176 }
5177
5178 fn finish(&mut self, _node: NodeMut<'_>) {}
5179 }
5180
5181 let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5182 let found = dfs_mut(ast, &mut marker).is_break();
5183 if !found {
5184 return Err(KclError::refactor(format!(
5185 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5186 )));
5187 }
5188
5189 Ok(())
5190}
5191
5192fn sketch_on_ast_expr(
5193 ast: &mut ast::Node<ast::Program>,
5194 scene_graph: &SceneGraph,
5195 solid_references: &HashMap<Uuid, SolidAstReference>,
5196 on: &Plane,
5197) -> Result<ast::Expr, KclError> {
5198 match on {
5199 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5200 Plane::Object(object_id) => {
5201 let on_object = scene_graph
5202 .objects
5203 .get(object_id.0)
5204 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5205 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5206 return Ok(face_expr);
5207 }
5208 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5209 }
5210 Plane::PrimitiveFace(face) => {
5211 let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5212 KclError::refactor(format!(
5213 "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5214 face.solid_id
5215 ))
5216 })?;
5217 let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5218 Ok(create_face_of_ast(solid_expr, face_id_expr))
5219 }
5220 }
5221}
5222
5223fn solid_references_from_variables(
5224 ast: &ast::Node<ast::Program>,
5225 variables: &IndexMap<String, KclValueView>,
5226) -> HashMap<Uuid, SolidAstReference> {
5227 let mut references = HashMap::new();
5228
5229 for item in &ast.body {
5232 let ast::BodyItem::VariableDeclaration(declaration) = item else {
5233 continue;
5234 };
5235 let name = &declaration.declaration.id.name;
5236 let Some(value) = variables.get(name) else {
5237 continue;
5238 };
5239
5240 match value {
5241 KclValueView::Solid { value } => {
5242 references.insert(
5243 value.id,
5244 SolidAstReference {
5245 variable_name: name.clone(),
5246 output_index: None,
5247 },
5248 );
5249 }
5250 KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5251 for (output_index, entry) in value.iter().enumerate() {
5252 if let KclValueView::Solid { value } = entry {
5253 references.insert(
5254 value.id,
5255 SolidAstReference {
5256 variable_name: name.clone(),
5257 output_index: Some(output_index),
5258 },
5259 );
5260 }
5261 }
5262 }
5263 _ => {}
5264 }
5265 }
5266
5267 references
5268}
5269
5270fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5271 let reference = solid_references.get(&solid_id)?;
5272 let solid_expr = ast_name_expr(reference.variable_name.clone());
5273 Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5274}
5275
5276fn sketch_face_of_scene_object_ast_expr(
5277 ast: &mut ast::Node<ast::Program>,
5278 on_object: &crate::front::Object,
5279) -> Result<Option<ast::Expr>, KclError> {
5280 match &on_object.kind {
5281 ObjectKind::Wall(wall) => {
5282 let solid_ref = get_or_insert_ast_reference(
5283 ast,
5284 &source_ref_from_source_ref_range(&wall.source.solid),
5285 "solid",
5286 None,
5287 )?;
5288 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5289 return Err(KclError::refactor(format!(
5290 "Could not resolve solid reference for selected wall: artifact_id={:?}",
5291 on_object.artifact_id
5292 )));
5293 };
5294 let solid_expr = indexed_solid_expr_for_sweep_output(
5295 ast_name_expr(solid_name_expr.name.name.clone()),
5296 wall.solid_output_index,
5297 );
5298 let sweep_ref = get_or_insert_ast_reference(
5299 ast,
5300 &source_ref_from_source_ref_range(&wall.source.sweep),
5301 "solid",
5302 None,
5303 )?;
5304 let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5305 return Err(KclError::refactor(format!(
5306 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5307 on_object.artifact_id
5308 )));
5309 };
5310 let sweep_name = sweep_name_expr.name.name.clone();
5311 let segment_ref = get_or_insert_ast_reference(
5312 ast,
5313 &source_ref_from_source_ref_range(&wall.source.segment),
5314 LINE_VARIABLE,
5315 None,
5316 )?;
5317
5318 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5319 wall.source
5320 .path
5321 .as_ref()
5322 .and_then(|path_source| region_name_from_path_source(ast, path_source))
5323 }) {
5324 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5325 return Err(KclError::refactor(format!(
5326 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5327 on_object.artifact_id
5328 )));
5329 };
5330 create_member_expression(
5331 create_member_expression(ast_name_expr(region_name), "tags"),
5332 &segment_name_expr.name.name,
5333 )
5334 } else {
5335 segment_ref
5336 };
5337
5338 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5339 }
5340 ObjectKind::Cap(cap) => {
5341 let solid_ref =
5342 get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5343 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5344 return Err(KclError::refactor(format!(
5345 "Could not resolve solid reference for selected cap: artifact_id={:?}",
5346 on_object.artifact_id
5347 )));
5348 };
5349 let solid_expr = indexed_solid_expr_for_sweep_output(
5350 ast_name_expr(solid_name_expr.name.name.clone()),
5351 cap.solid_output_index,
5352 );
5353 let face_expr = match cap.kind {
5355 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5356 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5357 };
5358
5359 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5360 }
5361 _ => Ok(None),
5362 }
5363}
5364
5365fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5366 match solid_output_index {
5367 Some(output_index) => create_index_expression(solid_expr, output_index),
5368 None => solid_expr,
5369 }
5370}
5371
5372fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5373 SourceRef::Simple {
5374 range: source.range,
5375 node_path: source.node_path.clone(),
5376 }
5377}
5378
5379fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5380 let source_ref = source_ref_from_source_ref_range(path_source);
5381 let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5382 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5383 return None;
5384 };
5385 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5386 return None;
5387 };
5388 if region_call.callee.name.name != "region" {
5389 return None;
5390 }
5391 Some(candidate)
5392}
5393
5394fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5395 let mut current_id = source_id;
5396 let mut current_composite = None;
5397 let mut visited = HashSet::new();
5398
5399 while visited.insert(current_id) {
5400 let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5401
5402 let Some(composite_id) = next_composite_id else {
5403 break;
5404 };
5405 let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5406 break;
5407 };
5408
5409 current_id = composite.id;
5410 current_composite = Some(composite);
5411
5412 if !composite.consumed {
5413 break;
5414 }
5415 }
5416
5417 current_composite.map(|composite| &composite.code_ref)
5418}
5419
5420fn downstream_composite_id_for_solid_source(
5421 artifact_graph: &ArtifactGraph,
5422 source_id: ArtifactId,
5423) -> Option<ArtifactId> {
5424 if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5426 && let Some(composite_id) = path.composite_solid_id
5427 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5428 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5429 {
5430 return Some(composite_id);
5431 }
5432
5433 for artifact in artifact_graph.values() {
5435 if let Artifact::Path(path) = artifact
5436 && path.sweep_id == Some(source_id)
5437 && let Some(composite_id) = path.composite_solid_id
5438 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5439 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5440 {
5441 return Some(composite_id);
5442 }
5443 }
5444
5445 artifact_graph.values().find_map(|artifact| {
5447 let Artifact::CompositeSolid(composite) = artifact else {
5448 return None;
5449 };
5450 composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5451 })
5452}
5453
5454fn composite_contains_path_input(
5455 solid_ids: &[ArtifactId],
5456 tool_ids: &[ArtifactId],
5457 path_id: ArtifactId,
5458 solid2d_id: Option<ArtifactId>,
5459) -> bool {
5460 composite_contains_input(solid_ids, tool_ids, path_id)
5461 || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5462}
5463
5464fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5465 solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5466}
5467
5468fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5469 let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5470 SourceRefRange {
5471 range: code_ref.range,
5472 node_path,
5473 }
5474}
5475
5476fn solid_output_index_for_sweep(
5477 artifact_graph: &ArtifactGraph,
5478 sweep_id: ArtifactId,
5479 sweep_code_ref: &CodeRef,
5480) -> Option<usize> {
5481 if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5484 return None;
5485 }
5486
5487 let sibling_sweeps = artifact_graph
5488 .values()
5489 .filter_map(|artifact| match artifact {
5490 Artifact::Sweep(sweep)
5491 if sweep.code_ref.range == sweep_code_ref.range
5492 && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5493 {
5494 Some(sweep)
5495 }
5496 _ => None,
5497 })
5498 .collect::<Vec<_>>();
5499
5500 if sibling_sweeps.len() <= 1 {
5501 return None;
5502 }
5503
5504 sibling_sweeps
5505 .iter()
5506 .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5507}
5508
5509fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5510 let mut existing_artifact_ids = scene_objects
5511 .iter()
5512 .map(|object| object.artifact_id)
5513 .collect::<HashSet<_>>();
5514
5515 for artifact in artifact_graph.values() {
5516 match artifact {
5517 Artifact::Wall(wall) => {
5518 if existing_artifact_ids.contains(&wall.id) {
5519 continue;
5520 }
5521
5522 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5523 Artifact::Segment(segment) => Some(segment),
5524 _ => None,
5525 }) else {
5526 continue;
5527 };
5528 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5529 Artifact::Sweep(sweep) => Some(sweep),
5530 _ => None,
5531 }) else {
5532 continue;
5533 };
5534 let source_segment = segment
5535 .original_seg_id
5536 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5537 .and_then(|artifact| match artifact {
5538 Artifact::Segment(segment) => Some(segment),
5539 _ => None,
5540 })
5541 .unwrap_or(segment);
5542 let solid_code_ref =
5543 downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5544 let path_code_ref = artifact_graph
5545 .get(&segment.path_id)
5546 .or_else(|| artifact_graph.get(&sweep.path_id))
5547 .and_then(|artifact| match artifact {
5548 Artifact::Path(path) => Some(&path.code_ref),
5549 _ => None,
5550 });
5551 let source = WallSource {
5552 solid: code_ref_source_ref_range(solid_code_ref),
5553 sweep: code_ref_source_ref_range(&sweep.code_ref),
5554 path: path_code_ref.map(code_ref_source_ref_range),
5555 segment: code_ref_source_ref_range(&source_segment.code_ref),
5556 };
5557 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5558 && solid_code_ref.node_path == sweep.code_ref.node_path)
5559 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5560 .flatten();
5561 let object_source = source_ref_from_source_ref_range(&source.solid);
5562 let id = ObjectId(scene_objects.len());
5563 scene_objects.push(crate::front::Object {
5564 id,
5565 kind: ObjectKind::Wall(crate::frontend::api::Wall {
5566 id,
5567 source,
5568 solid_output_index,
5569 }),
5570 label: Default::default(),
5571 comments: Default::default(),
5572 artifact_id: wall.id,
5573 source: object_source,
5574 });
5575 existing_artifact_ids.insert(wall.id);
5576 }
5577 Artifact::Cap(cap) => {
5578 if existing_artifact_ids.contains(&cap.id) {
5579 continue;
5580 }
5581
5582 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5583 Artifact::Sweep(sweep) => Some(sweep),
5584 _ => None,
5585 }) else {
5586 continue;
5587 };
5588 let id = ObjectId(scene_objects.len());
5589 let kind = match cap.sub_type {
5590 CapSubType::Start => crate::frontend::api::CapKind::Start,
5591 CapSubType::End => crate::frontend::api::CapKind::End,
5592 };
5593 let solid_code_ref =
5594 downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5595 let source = CapSource {
5596 solid: code_ref_source_ref_range(solid_code_ref),
5597 sweep: code_ref_source_ref_range(&sweep.code_ref),
5598 };
5599 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5600 && solid_code_ref.node_path == sweep.code_ref.node_path)
5601 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5602 .flatten();
5603 let object_source = source_ref_from_source_ref_range(&source.solid);
5604 scene_objects.push(crate::front::Object {
5605 id,
5606 kind: ObjectKind::Cap(crate::frontend::api::Cap {
5607 id,
5608 kind,
5609 source,
5610 solid_output_index,
5611 }),
5612 label: Default::default(),
5613 comments: Default::default(),
5614 artifact_id: cap.id,
5615 source: object_source,
5616 });
5617 existing_artifact_ids.insert(cap.id);
5618 }
5619 _ => {}
5620 }
5621 }
5622}
5623
5624fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5625 use crate::engine::PlaneName;
5626
5627 match name {
5628 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5629 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5630 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5631 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5632 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5633 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5634 }
5635}
5636
5637fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5638 ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5639 ast::UnaryOperator::Neg,
5640 ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5641 )))
5642}
5643
5644fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5645 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5646 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5647 unlabeled: Some(solid_expr),
5648 arguments: vec![ast::LabeledArg {
5649 label: Some(ast::Identifier::new("face")),
5650 arg: face_expr,
5651 }],
5652 digest: None,
5653 non_code_meta: Default::default(),
5654 })))
5655}
5656
5657fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5658 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5659 callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5660 unlabeled: Some(solid_expr),
5661 arguments: vec![ast::LabeledArg {
5662 label: Some(ast::Identifier::new("index")),
5663 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5664 ast::NumericLiteral {
5665 value: index as f64,
5666 suffix: NumericSuffix::None,
5667 raw: index.to_string(),
5668 digest: None,
5669 },
5670 )))),
5671 }],
5672 digest: None,
5673 non_code_meta: Default::default(),
5674 })))
5675}
5676
5677fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5678 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5679 return None;
5680 };
5681 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5682 return None;
5683 };
5684 if !matches!(
5685 sweep_call.callee.name.name.as_str(),
5686 "extrude" | "revolve" | "sweep" | "loft"
5687 ) {
5688 return None;
5689 }
5690 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5691 return None;
5692 };
5693 let candidate = region_name_expr.name.name.clone();
5694 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5695 return None;
5696 };
5697 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5698 return None;
5699 };
5700 if region_call.callee.name.name != "region" {
5701 return None;
5702 }
5703 Some(candidate)
5704}
5705
5706fn get_or_insert_ast_reference(
5713 ast: &mut ast::Node<ast::Program>,
5714 source_ref: &SourceRef,
5715 prefix: &str,
5716 property: Option<&str>,
5717) -> Result<ast::Expr, KclError> {
5718 let command = AstMutateCommand::AddVariableDeclaration {
5719 prefix: prefix.to_owned(),
5720 };
5721 let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5722 Ok((_, ret)) => ret,
5723 Err(err) => {
5724 if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5725 AstMutateCommandReturn::Name(var_name)
5726 } else {
5727 return Err(err);
5728 }
5729 }
5730 };
5731 let AstMutateCommandReturn::Name(var_name) = ret else {
5732 return Err(KclError::refactor(
5733 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5734 ));
5735 };
5736 let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5737 let Some(property) = property else {
5738 return Ok(var_expr);
5740 };
5741
5742 Ok(create_member_expression(var_expr, property))
5743}
5744
5745fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5746 let source_range = match source_ref {
5747 SourceRef::Simple { range, .. } => *range,
5748 SourceRef::BackTrace { ranges } => {
5749 let [range] = ranges.as_slice() else {
5750 return None;
5751 };
5752 range.0
5753 }
5754 };
5755 ast.body.iter().find_map(|item| {
5756 let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5757 return None;
5758 };
5759 let init_range = SourceRange::from(&var_decl.declaration.init);
5760 let source_is_inside_init = init_range.module_id() == source_range.module_id()
5761 && init_range.start() <= source_range.start()
5762 && source_range.end() <= init_range.end();
5763 if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5764 && init_range != source_range
5765 && source_is_inside_init
5766 {
5767 return None;
5768 }
5769 source_is_inside_init.then(|| var_decl.name().to_owned())
5770 })
5771}
5772
5773fn mutate_ast_node_by_source_ref(
5774 ast: &mut ast::Node<ast::Program>,
5775 source_ref: &SourceRef,
5776 command: AstMutateCommand,
5777) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5778 let (source_range, node_path) = match source_ref {
5779 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5780 SourceRef::BackTrace { ranges } => {
5781 let [range] = ranges.as_slice() else {
5782 return Err(KclError::refactor(format!(
5783 "Expected single source ref, got {}; ranges={ranges:#?}",
5784 ranges.len(),
5785 )));
5786 };
5787 (range.0, range.1.clone())
5788 }
5789 };
5790 let mut context = AstMutateContext {
5791 source_range,
5792 node_path,
5793 command,
5794 defined_names_stack: Default::default(),
5795 };
5796 let control = dfs_mut(ast, &mut context);
5797 match control {
5798 ControlFlow::Continue(_) => Err(KclError::refactor(
5799 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5800 )),
5801 ControlFlow::Break(break_value) => break_value,
5802 }
5803}
5804
5805#[derive(Debug)]
5806struct AstMutateContext {
5807 source_range: SourceRange,
5808 node_path: Option<ast::NodePath>,
5809 command: AstMutateCommand,
5810 defined_names_stack: Vec<HashSet<String>>,
5811}
5812
5813#[derive(Debug)]
5814#[allow(clippy::large_enum_variant)]
5815enum AstMutateCommand {
5816 AddSketchBlockExprStmt {
5818 expr: ast::Expr,
5819 },
5820 AddSketchBlockVarDecl {
5822 prefix: String,
5823 expr: ast::Expr,
5824 },
5825 AddVariableDeclaration {
5826 prefix: String,
5827 },
5828 EditPoint {
5829 at: ast::Expr,
5830 },
5831 EditLine {
5832 start: ast::Expr,
5833 end: ast::Expr,
5834 construction: Option<bool>,
5835 },
5836 EditArc {
5837 start: ast::Expr,
5838 end: ast::Expr,
5839 center: ast::Expr,
5840 direction: Option<ArcDirection>,
5841 construction: Option<bool>,
5842 },
5843 EditCircle {
5844 start: ast::Expr,
5845 center: ast::Expr,
5846 construction: Option<bool>,
5847 },
5848 EditControlPointSpline {
5849 points: ast::Expr,
5850 construction: Option<bool>,
5851 },
5852 EditConstraintValue {
5853 value: ast::BinaryPart,
5854 },
5855 EditAngleConstraint {
5856 call: ast::BinaryPart,
5857 value: ast::BinaryPart,
5858 },
5859 EditDistanceConstraint {
5860 call: ast::BinaryPart,
5861 value: ast::BinaryPart,
5862 },
5863 EditDistanceConstraintLabelPosition {
5864 label_position: ast::Expr,
5865 },
5866 EditCallUnlabeled {
5867 arg: ast::Expr,
5868 },
5869 EditVarInitialValue {
5870 value: Number,
5871 },
5872 DeleteNode,
5873}
5874
5875impl AstMutateCommand {
5876 fn needs_defined_names_stack(&self) -> bool {
5877 matches!(
5878 self,
5879 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5880 )
5881 }
5882}
5883
5884#[derive(Debug)]
5885enum AstMutateCommandReturn {
5886 None,
5887 Name(String),
5888}
5889
5890#[derive(Debug, Clone)]
5891struct AstNodeRef {
5892 range: SourceRange,
5893 node_path: Option<ast::NodePath>,
5894}
5895
5896impl<T> From<&ast::Node<T>> for AstNodeRef {
5897 fn from(value: &ast::Node<T>) -> Self {
5898 AstNodeRef {
5899 range: value.into(),
5900 node_path: value.node_path.clone(),
5901 }
5902 }
5903}
5904
5905impl From<&ast::BodyItem> for AstNodeRef {
5906 fn from(value: &ast::BodyItem) -> Self {
5907 match value {
5908 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5909 range: node.into(),
5910 node_path: node.node_path.clone(),
5911 },
5912 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5913 range: node.into(),
5914 node_path: node.node_path.clone(),
5915 },
5916 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5917 range: node.into(),
5918 node_path: node.node_path.clone(),
5919 },
5920 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5921 range: node.into(),
5922 node_path: node.node_path.clone(),
5923 },
5924 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5925 range: node.into(),
5926 node_path: node.node_path.clone(),
5927 },
5928 }
5929 }
5930}
5931
5932impl From<&ast::Expr> for AstNodeRef {
5933 fn from(value: &ast::Expr) -> Self {
5934 AstNodeRef {
5935 range: SourceRange::from(value),
5936 node_path: value.node_path().cloned(),
5937 }
5938 }
5939}
5940
5941impl From<&AstMutateContext> for AstNodeRef {
5942 fn from(value: &AstMutateContext) -> Self {
5943 AstNodeRef {
5944 range: value.source_range,
5945 node_path: value.node_path.clone(),
5946 }
5947 }
5948}
5949
5950impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5951 type Error = crate::walk::AstNodeError;
5952
5953 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5954 Ok(AstNodeRef {
5955 range: SourceRange::try_from(value)?,
5956 node_path: value.try_into()?,
5957 })
5958 }
5959}
5960
5961impl From<AstNodeRef> for SourceRange {
5962 fn from(value: AstNodeRef) -> Self {
5963 value.range
5964 }
5965}
5966
5967impl Visitor for AstMutateContext {
5968 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5969 type Continue = ();
5970
5971 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5972 filter_and_process(self, node)
5973 }
5974
5975 fn finish(&mut self, node: NodeMut<'_>) {
5976 match &node {
5977 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5978 self.defined_names_stack.pop();
5979 }
5980 _ => {}
5981 }
5982 }
5983}
5984
5985fn filter_and_process(
5986 ctx: &mut AstMutateContext,
5987 node: NodeMut,
5988) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5989 let Ok(node_range) = SourceRange::try_from(&node) else {
5990 return TraversalReturn::new_continue(());
5992 };
5993 if let NodeMut::VariableDeclaration(var_decl) = &node {
5998 let expr_range = SourceRange::from(&var_decl.declaration.init);
5999 let expr_node_path = var_decl.declaration.init.node_path();
6000 if source_ref_matches(ctx, expr_range, expr_node_path) {
6001 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6002 return TraversalReturn::new_break(Ok((
6005 AstNodeRef::from(&**var_decl),
6006 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6007 )));
6008 }
6009 if let AstMutateCommand::DeleteNode = &ctx.command {
6010 return TraversalReturn {
6013 mutate_body_item: MutateBodyItem::Delete,
6014 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6015 };
6016 }
6017 }
6018 }
6019 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6022 let expr_range = SourceRange::from(&expr_stmt.expression);
6023 let expr_node_path = expr_stmt.expression.node_path();
6024 if source_ref_matches(ctx, expr_range, expr_node_path) {
6025 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6026 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6029 return TraversalReturn::new_continue(());
6030 };
6031 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6032 }
6033 if let AstMutateCommand::DeleteNode = &ctx.command {
6034 return TraversalReturn {
6037 mutate_body_item: MutateBodyItem::Delete,
6038 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6039 };
6040 }
6041 }
6042 }
6043
6044 if ctx.command.needs_defined_names_stack() {
6045 if let NodeMut::Program(program) = &node {
6046 ctx.defined_names_stack.push(find_defined_names(*program));
6047 } else if let NodeMut::SketchBlock(block) = &node {
6048 ctx.defined_names_stack.push(find_defined_names(&block.body));
6049 }
6050 }
6051
6052 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6054 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6055 return TraversalReturn::new_continue(());
6056 }
6057 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6058 return TraversalReturn::new_continue(());
6059 };
6060 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6061}
6062
6063fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6064 match &ctx.node_path {
6065 Some(target) => Some(target) == node_path,
6066 None => node_range == ctx.source_range,
6067 }
6068}
6069
6070fn is_angle_constraint_call_name(name: &str) -> bool {
6071 matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6072}
6073
6074fn is_distance_constraint_call_name(name: &str) -> bool {
6075 matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6076}
6077
6078fn is_constraint_call_name(name: &str) -> bool {
6079 matches!(
6080 name,
6081 DISTANCE_FN
6082 | HORIZONTAL_DISTANCE_FN
6083 | VERTICAL_DISTANCE_FN
6084 | RADIUS_FN
6085 | DIAMETER_FN
6086 | ANGLE_FN
6087 | ANGLE_DIMENSION_FN
6088 )
6089}
6090
6091fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6092 if let ast::BinaryPart::CallExpressionKw(call) = part
6093 && is_constraint_call_name(call.callee.name.name.as_str())
6094 {
6095 Some(call)
6096 } else {
6097 None
6098 }
6099}
6100
6101fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6102 match &ctx.command {
6103 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6104 if let NodeMut::SketchBlock(sketch_block) = node {
6105 sketch_block
6106 .body
6107 .items
6108 .push(ast::BodyItem::ExpressionStatement(ast::Node {
6109 inner: ast::ExpressionStatement {
6110 expression: expr.clone(),
6111 digest: None,
6112 },
6113 start: Default::default(),
6114 end: Default::default(),
6115 module_id: Default::default(),
6116 node_path: None,
6117 outer_attrs: Default::default(),
6118 pre_comments: Default::default(),
6119 comment_start: Default::default(),
6120 }));
6121 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6122 }
6123 }
6124 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6125 if let NodeMut::SketchBlock(sketch_block) = node {
6126 let empty_defined_names = HashSet::new();
6127 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6128 let Ok(name) = next_free_name(prefix, defined_names) else {
6129 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6130 };
6131 sketch_block
6132 .body
6133 .items
6134 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6135 ast::VariableDeclaration::new(
6136 ast::VariableDeclarator::new(&name, expr.clone()),
6137 ast::ItemVisibility::Default,
6138 ast::VariableKind::Const,
6139 ),
6140 ))));
6141 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6142 }
6143 }
6144 AstMutateCommand::AddVariableDeclaration { prefix } => {
6145 if let NodeMut::VariableDeclaration(inner) = node {
6146 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6147 }
6148 if let NodeMut::ExpressionStatement(expr_stmt) = node {
6149 let empty_defined_names = HashSet::new();
6150 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6151 let Ok(name) = next_free_name(prefix, defined_names) else {
6152 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6154 };
6155 let mutate_node =
6156 ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6157 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6158 ast::ItemVisibility::Default,
6159 ast::VariableKind::Const,
6160 ))));
6161 return TraversalReturn {
6162 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6163 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6164 };
6165 }
6166 }
6167 AstMutateCommand::EditPoint { at } => {
6168 if let NodeMut::CallExpressionKw(call) = node {
6169 if call.callee.name.name != POINT_FN {
6170 return TraversalReturn::new_continue(());
6171 }
6172 for labeled_arg in &mut call.arguments {
6174 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6175 labeled_arg.arg = at.clone();
6176 }
6177 }
6178 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6179 }
6180 }
6181 AstMutateCommand::EditLine {
6182 start,
6183 end,
6184 construction,
6185 } => {
6186 if let NodeMut::CallExpressionKw(call) = node {
6187 if call.callee.name.name != LINE_FN {
6188 return TraversalReturn::new_continue(());
6189 }
6190 for labeled_arg in &mut call.arguments {
6192 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6193 labeled_arg.arg = start.clone();
6194 }
6195 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6196 labeled_arg.arg = end.clone();
6197 }
6198 }
6199 if let Some(construction_value) = construction {
6201 let construction_exists = call
6202 .arguments
6203 .iter()
6204 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6205 if *construction_value {
6206 if construction_exists {
6208 for labeled_arg in &mut call.arguments {
6210 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6211 labeled_arg.arg =
6212 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6213 value: ast::LiteralValue::Bool(true),
6214 raw: "true".to_string(),
6215 digest: None,
6216 })));
6217 }
6218 }
6219 } else {
6220 call.arguments.push(ast::LabeledArg {
6222 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6223 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6224 value: ast::LiteralValue::Bool(true),
6225 raw: "true".to_string(),
6226 digest: None,
6227 }))),
6228 });
6229 }
6230 } else {
6231 call.arguments
6233 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6234 }
6235 }
6236 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6237 }
6238 }
6239 AstMutateCommand::EditArc {
6240 start,
6241 end,
6242 center,
6243 direction,
6244 construction,
6245 } => {
6246 if let NodeMut::CallExpressionKw(call) = node {
6247 if call.callee.name.name != ARC_FN {
6248 return TraversalReturn::new_continue(());
6249 }
6250 for labeled_arg in &mut call.arguments {
6252 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6253 labeled_arg.arg = start.clone();
6254 }
6255 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6256 labeled_arg.arg = end.clone();
6257 }
6258 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6259 labeled_arg.arg = center.clone();
6260 }
6261 }
6262 if let Some(direction_value) = direction {
6264 let direction_exists = call
6265 .arguments
6266 .iter()
6267 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6268 if direction_value.is_clockwise() {
6269 let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6270 if direction_exists {
6271 for labeled_arg in &mut call.arguments {
6273 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6274 labeled_arg.arg = direction_ast.clone();
6275 }
6276 }
6277 } else {
6278 call.arguments.push(ast::LabeledArg {
6280 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6281 arg: direction_ast,
6282 });
6283 }
6284 } else {
6285 call.arguments
6288 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6289 }
6290 }
6291 if let Some(construction_value) = construction {
6293 let construction_exists = call
6294 .arguments
6295 .iter()
6296 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6297 if *construction_value {
6298 if construction_exists {
6300 for labeled_arg in &mut call.arguments {
6302 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6303 labeled_arg.arg =
6304 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6305 value: ast::LiteralValue::Bool(true),
6306 raw: "true".to_string(),
6307 digest: None,
6308 })));
6309 }
6310 }
6311 } else {
6312 call.arguments.push(ast::LabeledArg {
6314 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6315 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6316 value: ast::LiteralValue::Bool(true),
6317 raw: "true".to_string(),
6318 digest: None,
6319 }))),
6320 });
6321 }
6322 } else {
6323 call.arguments
6325 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6326 }
6327 }
6328 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6329 }
6330 }
6331 AstMutateCommand::EditCircle {
6332 start,
6333 center,
6334 construction,
6335 } => {
6336 if let NodeMut::CallExpressionKw(call) = node {
6337 if call.callee.name.name != CIRCLE_FN {
6338 return TraversalReturn::new_continue(());
6339 }
6340 for labeled_arg in &mut call.arguments {
6342 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6343 labeled_arg.arg = start.clone();
6344 }
6345 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6346 labeled_arg.arg = center.clone();
6347 }
6348 }
6349 if let Some(construction_value) = construction {
6351 let construction_exists = call
6352 .arguments
6353 .iter()
6354 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6355 if *construction_value {
6356 if construction_exists {
6357 for labeled_arg in &mut call.arguments {
6359 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6360 labeled_arg.arg =
6361 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6362 value: ast::LiteralValue::Bool(true),
6363 raw: "true".to_string(),
6364 digest: None,
6365 })));
6366 }
6367 }
6368 } else {
6369 call.arguments.push(ast::LabeledArg {
6371 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6372 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6373 value: ast::LiteralValue::Bool(true),
6374 raw: "true".to_string(),
6375 digest: None,
6376 }))),
6377 });
6378 }
6379 } else {
6380 call.arguments
6382 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6383 }
6384 }
6385 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6386 }
6387 }
6388 AstMutateCommand::EditControlPointSpline { points, construction } => {
6389 if let NodeMut::CallExpressionKw(call) = node {
6390 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6391 return TraversalReturn::new_continue(());
6392 }
6393 for labeled_arg in &mut call.arguments {
6394 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6395 {
6396 labeled_arg.arg = points.clone();
6397 }
6398 }
6399 if let Some(construction_value) = construction {
6401 let construction_exists = call
6402 .arguments
6403 .iter()
6404 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6405 if *construction_value {
6406 if construction_exists {
6407 for labeled_arg in &mut call.arguments {
6408 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6409 labeled_arg.arg =
6410 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6411 value: ast::LiteralValue::Bool(true),
6412 raw: "true".to_string(),
6413 digest: None,
6414 })));
6415 }
6416 }
6417 } else {
6418 call.arguments.push(ast::LabeledArg {
6419 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6420 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6421 value: ast::LiteralValue::Bool(true),
6422 raw: "true".to_string(),
6423 digest: None,
6424 }))),
6425 });
6426 }
6427 } else {
6428 call.arguments
6429 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6430 }
6431 }
6432 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6433 }
6434 }
6435 AstMutateCommand::EditConstraintValue { value } => {
6436 if let NodeMut::BinaryExpression(binary_expr) = node {
6437 let left_is_constraint = matches!(
6438 &binary_expr.left,
6439 ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6440 );
6441 if left_is_constraint {
6442 binary_expr.right = value.clone();
6443 } else {
6444 binary_expr.left = value.clone();
6445 }
6446
6447 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6448 }
6449 }
6450 AstMutateCommand::EditAngleConstraint { call, value } => {
6451 if let NodeMut::BinaryExpression(binary_expr) = node {
6452 let left_is_angle = matches!(
6453 &binary_expr.left,
6454 ast::BinaryPart::CallExpressionKw(existing_call)
6455 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6456 );
6457 let right_is_angle = matches!(
6458 &binary_expr.right,
6459 ast::BinaryPart::CallExpressionKw(existing_call)
6460 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6461 );
6462
6463 match (left_is_angle, right_is_angle) {
6464 (true, _) => {
6465 binary_expr.left = call.clone();
6466 binary_expr.right = value.clone();
6467 }
6468 (false, true) => {
6469 binary_expr.left = value.clone();
6470 binary_expr.right = call.clone();
6471 }
6472 (false, false) => return TraversalReturn::new_continue(()),
6473 }
6474
6475 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6476 }
6477 }
6478 AstMutateCommand::EditDistanceConstraint { call, value } => {
6479 if let NodeMut::BinaryExpression(binary_expr) = node {
6480 let left_is_distance = matches!(
6481 &binary_expr.left,
6482 ast::BinaryPart::CallExpressionKw(existing_call)
6483 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6484 );
6485 let right_is_distance = matches!(
6486 &binary_expr.right,
6487 ast::BinaryPart::CallExpressionKw(existing_call)
6488 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6489 );
6490
6491 match (left_is_distance, right_is_distance) {
6492 (true, _) => {
6493 binary_expr.left = call.clone();
6494 binary_expr.right = value.clone();
6495 }
6496 (false, true) => {
6497 binary_expr.left = value.clone();
6498 binary_expr.right = call.clone();
6499 }
6500 (false, false) => return TraversalReturn::new_continue(()),
6501 }
6502
6503 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6504 }
6505 }
6506 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6507 if let NodeMut::BinaryExpression(binary_expr) = node {
6508 let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6509 call
6510 } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6511 call
6512 } else {
6513 return TraversalReturn::new_continue(());
6514 };
6515
6516 if let Some(label_arg) = call
6517 .arguments
6518 .iter_mut()
6519 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6520 {
6521 label_arg.arg = label_position.clone();
6522 } else {
6523 call.arguments.push(ast::LabeledArg {
6524 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6525 arg: label_position.clone(),
6526 });
6527 }
6528
6529 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6530 }
6531 }
6532 AstMutateCommand::EditCallUnlabeled { arg } => {
6533 if let NodeMut::CallExpressionKw(call) = node {
6534 call.unlabeled = Some(arg.clone());
6535 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6536 }
6537 }
6538 AstMutateCommand::EditVarInitialValue { value } => {
6539 if let NodeMut::SketchVar(sketch_var) = node {
6543 let Ok(literal) = to_source_number(*value) else {
6544 return TraversalReturn::new_break(Err(KclError::refactor(format!(
6545 "Could not convert number to AST literal: {:?}",
6546 *value
6547 ))));
6548 };
6549 sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6550 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6551 }
6552 }
6553 AstMutateCommand::DeleteNode => {
6554 return TraversalReturn {
6555 mutate_body_item: MutateBodyItem::Delete,
6556 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6557 };
6558 }
6559 }
6560 TraversalReturn::new_continue(())
6561}
6562
6563struct FindSketchBlockSourceRange {
6564 target_before_mutation: SourceRange,
6566 found: Cell<Option<AstNodeRef>>,
6570}
6571
6572impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6573 type Error = crate::front::Error;
6574
6575 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6576 let Ok(node_range) = SourceRange::try_from(&node) else {
6577 return Ok(true);
6578 };
6579
6580 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6581 if node_range.module_id() == self.target_before_mutation.module_id()
6582 && node_range.start() == self.target_before_mutation.start()
6583 && node_range.end() >= self.target_before_mutation.end()
6585 {
6586 self.found.set(sketch_block.body.items.last().map(|item| match item {
6587 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6591 _ => AstNodeRef::from(item),
6592 }));
6593 return Ok(false);
6594 } else {
6595 return Ok(true);
6598 }
6599 }
6600
6601 for child in node.children().iter() {
6602 if !child.visit(*self)? {
6603 return Ok(false);
6604 }
6605 }
6606
6607 Ok(true)
6608 }
6609}
6610
6611struct FindSketchBlockByNodePath {
6612 target_node_path: ast::NodePath,
6614 found: Cell<Option<AstNodeRef>>,
6618}
6619
6620impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6621 type Error = crate::front::Error;
6622
6623 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6624 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6625 return Ok(true);
6626 };
6627
6628 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6629 if let Some(node_path) = node_path
6630 && node_path == self.target_node_path
6631 {
6632 self.found.set(sketch_block.body.items.last().map(|item| match item {
6633 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6637 _ => AstNodeRef::from(item),
6638 }));
6639
6640 return Ok(false);
6641 } else {
6642 return Ok(true);
6645 }
6646 }
6647
6648 for child in node.children().iter() {
6649 if !child.visit(*self)? {
6650 return Ok(false);
6651 }
6652 }
6653
6654 Ok(true)
6655 }
6656}
6657
6658fn find_sketch_block_added_item(
6666 ast: &ast::Node<ast::Program>,
6667 sketch_block_before_mutation: &AstNodeRef,
6668) -> Result<AstNodeRef, KclError> {
6669 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6670 let find = FindSketchBlockByNodePath {
6671 target_node_path: node_path.clone(),
6672 found: Cell::new(None),
6673 };
6674 let node = crate::walk::Node::from(ast);
6675 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6676 find.found.into_inner().ok_or_else(|| {
6677 KclError::refactor(format!(
6678 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6679 ))
6680 })
6681 } else {
6682 let find = FindSketchBlockSourceRange {
6684 target_before_mutation: sketch_block_before_mutation.range,
6685 found: Cell::new(None),
6686 };
6687 let node = crate::walk::Node::from(ast);
6688 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6689 find.found.into_inner().ok_or_else(|| KclError::refactor(
6690 format!("Source range after mutation not found for range before mutation: {sketch_block_before_mutation:?}; Did you try formatting (i.e. call recast) before calling this?"),
6691 ))
6692 }
6693}
6694
6695fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6696 let message = error.message().trim();
6697 let message = if message.is_empty() {
6698 "unknown parse error"
6699 } else {
6700 message
6701 };
6702
6703 format!("{prefix}: {message}")
6704}
6705
6706fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6707 let (program, errors) = Program::parse(source).map_err(|err| {
6708 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6709 })?;
6710 if !errors.is_empty() {
6711 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6712 format_compilation_issues(parse_error_prefix, &errors),
6713 )));
6714 }
6715
6716 program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6717}
6718
6719fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6720 let Some(first_issue) = issues
6721 .iter()
6722 .find(|issue| issue.severity.is_err())
6723 .or_else(|| issues.first())
6724 else {
6725 return prefix.to_owned();
6726 };
6727
6728 let message = first_issue.message.trim();
6729 let message = if message.is_empty() {
6730 "unknown parse error"
6731 } else {
6732 message
6733 };
6734
6735 if issues.len() > 1 {
6736 format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6737 } else {
6738 format!("{prefix}: {message}")
6739 }
6740}
6741
6742fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6743 ast.recast_top(&Default::default(), 0)
6745}
6746
6747struct FindNumericLiteral {
6748 target: SourceRange,
6749 found: Cell<Option<ast::NumericLiteral>>,
6750}
6751
6752impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6753 type Error = crate::front::Error;
6754
6755 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6756 let Ok(node_range) = SourceRange::try_from(&node) else {
6757 return Ok(true);
6758 };
6759
6760 if node_range == self.target
6761 && let crate::walk::Node::NumericLiteral(literal) = node
6762 {
6763 self.found.set(Some(literal.inner.clone()));
6764 return Ok(false);
6765 }
6766
6767 for child in node.children().iter() {
6768 if !child.visit(*self)? {
6769 return Ok(false);
6770 }
6771 }
6772
6773 Ok(true)
6774 }
6775}
6776
6777fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6778 let find = FindNumericLiteral {
6779 target,
6780 found: Cell::new(None),
6781 };
6782 let node = crate::walk::Node::from(ast);
6783 node.visit(&find).ok()?;
6784 find.found.into_inner()
6785}
6786
6787struct FindSketchVarInitialByNodePath<'a> {
6788 target: &'a ast::NodePath,
6789 sketch_var_found: Cell<bool>,
6790 initial_literal: Cell<Option<ast::NumericLiteral>>,
6791}
6792
6793impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6794 type Error = crate::front::Error;
6795
6796 fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6797 if let crate::walk::Node::SketchVar(sketch_var) = node
6798 && sketch_var.node_path.as_ref() == Some(self.target)
6799 {
6800 self.sketch_var_found.set(true);
6801 if let Some(initial) = &sketch_var.initial {
6802 self.initial_literal.set(Some(initial.inner.clone()));
6803 }
6804 return Ok(false);
6805 }
6806
6807 for child in node.children().iter() {
6808 if !child.visit(*self)? {
6809 return Ok(false);
6810 }
6811 }
6812
6813 Ok(true)
6814 }
6815}
6816
6817fn numeric_literal_at_node_path(
6827 ast: &ast::Node<ast::Program>,
6828 node_path: Option<&ast::NodePath>,
6829 source_range: SourceRange,
6830) -> Option<Option<ast::NumericLiteral>> {
6831 let Some(node_path) = node_path else {
6832 let message = "numeric_literal_at_node_path: missing node_path on var solution; falling back to source-range lookup, which can fail under whitespace shifts";
6833 #[cfg(target_arch = "wasm32")]
6834 web_sys::console::warn_1(&message.into());
6835 #[cfg(not(target_arch = "wasm32"))]
6836 eprintln!("WARNING: {message}");
6837 return numeric_literal_at_source_range(ast, source_range).map(Some);
6838 };
6839 let find = FindSketchVarInitialByNodePath {
6840 target: node_path,
6841 sketch_var_found: Cell::new(false),
6842 initial_literal: Cell::new(None),
6843 };
6844 let node = crate::walk::Node::from(ast);
6845 node.visit(&find).ok()?;
6846 if !find.sketch_var_found.get() {
6847 return None;
6848 }
6849 Some(find.initial_literal.into_inner())
6850}
6851
6852fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6853 match suffix {
6854 NumericSuffix::Mm => Some(UnitLength::Millimeters),
6855 NumericSuffix::Cm => Some(UnitLength::Centimeters),
6856 NumericSuffix::M => Some(UnitLength::Meters),
6857 NumericSuffix::Inch => Some(UnitLength::Inches),
6858 NumericSuffix::Ft => Some(UnitLength::Feet),
6859 NumericSuffix::Yd => Some(UnitLength::Yards),
6860 _ => None,
6861 }
6862}
6863
6864fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6865 match suffix_length_unit(number.units) {
6866 Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6867 None => number.value,
6868 }
6869}
6870
6871fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6872 match suffix_length_unit(literal.suffix) {
6873 Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6874 None => literal.value,
6875 }
6876}
6877
6878fn var_solution_needs_commit(
6879 current_literal: &ast::NumericLiteral,
6880 solved_value: Number,
6881 default_length_unit: UnitLength,
6882) -> bool {
6883 let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6884 let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6885
6886 (current - solved).abs() > 1e-9
6887}
6888
6889fn preserve_var_solution_literal_style(
6890 current_literal: &ast::NumericLiteral,
6891 solved_value: Number,
6892 default_length_unit: UnitLength,
6893) -> Number {
6894 if current_literal.suffix == NumericSuffix::None {
6895 return Number {
6896 value: number_value_in_default_length_units(solved_value, default_length_unit),
6897 units: NumericSuffix::None,
6898 };
6899 }
6900
6901 let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6902 return solved_value;
6903 };
6904
6905 let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6906 Number {
6907 value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6908 units: current_literal.suffix,
6909 }
6910}
6911
6912pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6913 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6914 inner: ast::ArrayExpression {
6915 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6916 non_code_meta: Default::default(),
6917 digest: None,
6918 },
6919 start: Default::default(),
6920 end: Default::default(),
6921 module_id: Default::default(),
6922 node_path: None,
6923 outer_attrs: Default::default(),
6924 pre_comments: Default::default(),
6925 comment_start: Default::default(),
6926 })))
6927}
6928
6929pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6930 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6931 ast::ArrayExpression {
6932 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6933 digest: None,
6934 non_code_meta: Default::default(),
6935 },
6936 ))))
6937}
6938
6939fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6940 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6941 ast::ArrayExpression {
6942 elements: vec![
6943 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6944 point.x,
6945 )?)))),
6946 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6947 point.y,
6948 )?)))),
6949 ],
6950 non_code_meta: Default::default(),
6951 digest: None,
6952 },
6953 ))))
6954}
6955
6956fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6957 match expr {
6958 Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6959 inner: ast::Literal::from(to_source_number(*number)?),
6960 start: Default::default(),
6961 end: Default::default(),
6962 module_id: Default::default(),
6963 node_path: None,
6964 outer_attrs: Default::default(),
6965 pre_comments: Default::default(),
6966 comment_start: Default::default(),
6967 }))),
6968 Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6969 inner: ast::SketchVar {
6970 initial: Some(BoxNode::new(ast::Node {
6971 inner: to_source_number(*number)?,
6972 start: Default::default(),
6973 end: Default::default(),
6974 module_id: Default::default(),
6975 node_path: None,
6976 outer_attrs: Default::default(),
6977 pre_comments: Default::default(),
6978 comment_start: Default::default(),
6979 })),
6980 digest: None,
6981 },
6982 start: Default::default(),
6983 end: Default::default(),
6984 module_id: Default::default(),
6985 node_path: None,
6986 outer_attrs: Default::default(),
6987 pre_comments: Default::default(),
6988 comment_start: Default::default(),
6989 }))),
6990 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6991 }
6992}
6993
6994fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6995 Ok(ast::NumericLiteral {
6996 value: number.value,
6997 suffix: number.units,
6998 raw: format_number_literal(number.value, number.units, None)?,
6999 digest: None,
7000 })
7001}
7002
7003pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7004 ast::Expr::Name(BoxNode::new(ast_name(name)))
7005}
7006
7007fn ast_name(name: String) -> ast::Node<ast::Name> {
7008 ast::Node {
7009 inner: ast::Name {
7010 name: ast::Node {
7011 inner: ast::Identifier { name, digest: None },
7012 start: Default::default(),
7013 end: Default::default(),
7014 module_id: Default::default(),
7015 node_path: None,
7016 outer_attrs: Default::default(),
7017 pre_comments: Default::default(),
7018 comment_start: Default::default(),
7019 },
7020 path: Vec::new(),
7021 abs_path: false,
7022 digest: None,
7023 },
7024 start: Default::default(),
7025 end: Default::default(),
7026 module_id: Default::default(),
7027 node_path: None,
7028 outer_attrs: Default::default(),
7029 pre_comments: Default::default(),
7030 comment_start: Default::default(),
7031 }
7032}
7033
7034pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7035 ast::Name {
7036 name: ast::Node {
7037 inner: ast::Identifier {
7038 name: name.to_owned(),
7039 digest: None,
7040 },
7041 start: Default::default(),
7042 end: Default::default(),
7043 module_id: Default::default(),
7044 node_path: None,
7045 outer_attrs: Default::default(),
7046 pre_comments: Default::default(),
7047 comment_start: Default::default(),
7048 },
7049 path: Default::default(),
7050 abs_path: false,
7051 digest: None,
7052 }
7053}
7054
7055pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
7057 let elements = exprs.into_iter().collect::<Vec<_>>();
7058 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
7059
7060 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7062 elements,
7063 digest: None,
7064 non_code_meta: Default::default(),
7065 })));
7066
7067 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7069 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
7070 unlabeled: Some(array_expr),
7071 arguments: Default::default(),
7072 digest: None,
7073 non_code_meta: Default::default(),
7074 })))
7075}
7076
7077pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7079 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7080 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7081 unlabeled: Some(line_expr),
7082 arguments: Default::default(),
7083 digest: None,
7084 non_code_meta: Default::default(),
7085 })))
7086}
7087
7088pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7090 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7091 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7092 unlabeled: Some(line_expr),
7093 arguments: Default::default(),
7094 digest: None,
7095 non_code_meta: Default::default(),
7096 })))
7097}
7098
7099pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7101 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7102 object: object_expr,
7103 property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7104 name: ast::Node::no_src(ast::Identifier {
7105 name: property.to_string(),
7106 digest: None,
7107 }),
7108 path: Vec::new(),
7109 abs_path: false,
7110 digest: None,
7111 }))),
7112 computed: false,
7113 digest: None,
7114 })))
7115}
7116
7117pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7118 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7119 object: object_expr,
7120 property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7121 ast::NumericLiteral {
7122 value: index as f64,
7123 suffix: NumericSuffix::None,
7124 raw: index.to_string(),
7125 digest: None,
7126 },
7127 )))),
7128 computed: true,
7129 digest: None,
7130 })))
7131}
7132
7133fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7135 let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7137 position.x,
7138 )?))));
7139 let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7140 position.y,
7141 )?))));
7142 let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7143 elements: vec![x_literal, y_literal],
7144 digest: None,
7145 non_code_meta: Default::default(),
7146 })));
7147
7148 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7150 elements: vec![point_expr, point_array],
7151 digest: None,
7152 non_code_meta: Default::default(),
7153 })));
7154
7155 Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7157 ast::CallExpressionKw {
7158 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7159 unlabeled: Some(array_expr),
7160 arguments: Default::default(),
7161 digest: None,
7162 non_code_meta: Default::default(),
7163 },
7164 ))))
7165}
7166
7167pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7169 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7170 elements: line_exprs,
7171 digest: None,
7172 non_code_meta: Default::default(),
7173 })));
7174
7175 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7177 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7178 unlabeled: Some(array_expr),
7179 arguments: Default::default(),
7180 digest: None,
7181 non_code_meta: Default::default(),
7182 })))
7183}
7184
7185pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7187 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7188 elements: segment_exprs,
7189 digest: None,
7190 non_code_meta: Default::default(),
7191 })));
7192
7193 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7194 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7195 unlabeled: Some(array_expr),
7196 arguments: Default::default(),
7197 digest: None,
7198 non_code_meta: Default::default(),
7199 })))
7200}
7201
7202pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7204 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7205 elements: vec![seg1_expr, seg2_expr],
7206 digest: None,
7207 non_code_meta: Default::default(),
7208 })));
7209
7210 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7211 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7212 unlabeled: Some(array_expr),
7213 arguments: Default::default(),
7214 digest: None,
7215 non_code_meta: Default::default(),
7216 })))
7217}
7218
7219pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7221 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7222 elements: input_exprs,
7223 digest: None,
7224 non_code_meta: Default::default(),
7225 })));
7226 let arguments = vec![ast::LabeledArg {
7227 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7228 arg: axis_expr,
7229 }];
7230
7231 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7232 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7233 unlabeled: Some(array_expr),
7234 arguments,
7235 digest: None,
7236 non_code_meta: Default::default(),
7237 })))
7238}
7239
7240pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7242 let arguments = vec![ast::LabeledArg {
7243 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7244 arg: point_expr,
7245 }];
7246
7247 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7248 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7249 unlabeled: Some(segment_expr),
7250 arguments,
7251 digest: None,
7252 non_code_meta: Default::default(),
7253 })))
7254}
7255
7256fn issue_source_range(error: &KclError) -> SourceRange {
7261 let source_ranges = error.source_ranges();
7262 source_ranges
7263 .iter()
7264 .find(|range| range.is_top_level_module())
7265 .or_else(|| source_ranges.first())
7266 .copied()
7267 .unwrap_or_else(SourceRange::synthetic)
7268}
7269
7270#[cfg(test)]
7271mod tests {
7272 use std::sync;
7273
7274 use super::*;
7275 use crate::engine::PlaneName;
7276 use crate::engine::engine_manager::EngineManager;
7277 use crate::execution::cache::SketchModeState;
7278 use crate::execution::cache::clear_mem_cache;
7279 use crate::execution::cache::read_old_memory;
7280 use crate::execution::cache::write_old_memory;
7281 use crate::front::Distance;
7282 use crate::front::Fixed;
7283 use crate::front::FixedPoint;
7284 use crate::front::Midpoint;
7285 use crate::front::Object;
7286 use crate::front::Plane;
7287 use crate::front::Sketch;
7288 use crate::front::Tangent;
7289 use crate::frontend::sketch::Vertical;
7290 use crate::pretty::NumericSuffix;
7291
7292 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7293 for object in &scene_graph.objects {
7294 if let ObjectKind::Sketch(_) = &object.kind {
7295 return Some(object);
7296 }
7297 }
7298 None
7299 }
7300
7301 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7302 for object in &scene_graph.objects {
7303 if let ObjectKind::Face(_) = &object.kind {
7304 return Some(object);
7305 }
7306 }
7307 None
7308 }
7309
7310 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7311 for object in &scene_graph.objects {
7312 if matches!(&object.kind, ObjectKind::Wall(_)) {
7313 return Some(object.id);
7314 }
7315 }
7316 None
7317 }
7318
7319 fn find_cap_object_id_with_solid_output_index(
7320 scene_graph: &SceneGraph,
7321 cap_kind: crate::frontend::api::CapKind,
7322 solid_output_index: usize,
7323 ) -> Option<ObjectId> {
7324 for object in &scene_graph.objects {
7325 if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7326 {
7327 return Some(object.id);
7328 }
7329 }
7330 None
7331 }
7332
7333 #[test]
7334 fn issue_source_range_prefers_top_level_module() {
7335 use kcl_error::ModuleId;
7336
7337 let top = SourceRange::new(10, 20, ModuleId::default());
7338 let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7339
7340 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7343 "boom".to_owned(),
7344 vec![imported, top],
7345 ));
7346 assert_eq!(super::issue_source_range(&error), top);
7347
7348 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7350 assert_eq!(super::issue_source_range(&error), imported);
7351
7352 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7354 assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7355 }
7356
7357 #[test]
7358 fn composite_constituent_sweeps_are_not_solid_outputs() {
7359 use kcl_api::artifact::ArtifactSweepMethod;
7360 use kcl_api::artifact::CompositeSolid;
7361 use kcl_api::artifact::CompositeSolidSubType;
7362 use kcl_api::artifact::Sweep;
7363 use kcl_api::artifact::SweepSubType;
7364
7365 let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7366 let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7367 let composite_id = ArtifactId::new(Uuid::new_v4());
7368 let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7369 let sweep = |id| {
7370 Artifact::Sweep(Sweep {
7371 id,
7372 sub_type: SweepSubType::Extrusion,
7373 path_id: ArtifactId::new(Uuid::new_v4()),
7374 surface_ids: Vec::new(),
7375 edge_ids: Vec::new(),
7376 code_ref: code_ref.clone(),
7377 source_sweep_id: None,
7378 trajectory_id: None,
7379 method: ArtifactSweepMethod::New,
7380 consumed: false,
7381 pattern_ids: Vec::new(),
7382 })
7383 };
7384 let mut artifacts = IndexMap::from([
7385 (first_sweep_id, sweep(first_sweep_id)),
7386 (second_sweep_id, sweep(second_sweep_id)),
7387 ]);
7388
7389 let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7390 assert_eq!(
7391 solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7392 Some(0)
7393 );
7394 assert_eq!(
7395 solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7396 Some(1)
7397 );
7398
7399 artifacts.insert(
7400 composite_id,
7401 Artifact::CompositeSolid(CompositeSolid {
7402 id: composite_id,
7403 consumed: false,
7404 sub_type: CompositeSolidSubType::Union,
7405 output_index: None,
7406 solid_ids: vec![first_sweep_id, second_sweep_id],
7407 tool_ids: Vec::new(),
7408 code_ref,
7409 composite_solid_id: None,
7410 pattern_ids: Vec::new(),
7411 }),
7412 );
7413 let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7414 assert_eq!(
7415 solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7416 None
7417 );
7418 assert_eq!(
7419 solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7420 None
7421 );
7422 }
7423
7424 #[test]
7425 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7426 let source = "\
7427region001 = region(point = [0.1, 0.1], sketch = s)
7428extrude001 = extrude(region001, length = 5)
7429revolve001 = revolve(region001, axis = Y)
7430sweep001 = sweep(region001, path = path001)
7431loft001 = loft(region001)
7432not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7433";
7434
7435 let program = Program::parse(source).unwrap().0.unwrap();
7436
7437 assert_eq!(
7438 region_name_from_sweep_variable(&program.ast, "extrude001"),
7439 Some("region001".to_owned())
7440 );
7441 assert_eq!(
7442 region_name_from_sweep_variable(&program.ast, "revolve001"),
7443 Some("region001".to_owned())
7444 );
7445 assert_eq!(
7446 region_name_from_sweep_variable(&program.ast, "sweep001"),
7447 Some("region001".to_owned())
7448 );
7449 assert_eq!(
7450 region_name_from_sweep_variable(&program.ast, "loft001"),
7451 Some("region001".to_owned())
7452 );
7453 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7454 }
7455
7456 #[track_caller]
7457 fn expect_sketch(object: &Object) -> &Sketch {
7458 if let ObjectKind::Sketch(sketch) = &object.kind {
7459 sketch
7460 } else {
7461 panic!("Object is not a sketch: {:?}", object);
7462 }
7463 }
7464
7465 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7466 let point_object = scene_graph.objects.get(point_id.0).unwrap();
7467 let ObjectKind::Segment {
7468 segment: Segment::Point(point),
7469 } = &point_object.kind
7470 else {
7471 panic!("Object is not a point segment: {point_object:?}");
7472 };
7473 point.position.clone()
7474 }
7475
7476 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7477 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7478 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7479 }
7480
7481 fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7484 Point2d {
7485 x: Expr::Var(Number {
7486 value: x,
7487 units: NumericSuffix::Mm,
7488 }),
7489 y: Expr::Var(Number {
7490 value: y,
7491 units: NumericSuffix::Mm,
7492 }),
7493 }
7494 }
7495
7496 fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7499 Point2d {
7500 x: Number {
7501 value: x,
7502 units: NumericSuffix::Mm,
7503 },
7504 y: Number {
7505 value: y,
7506 units: NumericSuffix::Mm,
7507 },
7508 }
7509 }
7510
7511 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7512 LineCtor {
7513 start: Point2d {
7514 x: Expr::Number(Number { value: start_x, units }),
7515 y: Expr::Number(Number { value: start_y, units }),
7516 },
7517 end: Point2d {
7518 x: Expr::Number(Number { value: end_x, units }),
7519 y: Expr::Number(Number { value: end_y, units }),
7520 },
7521 construction: None,
7522 }
7523 }
7524
7525 async fn create_sketch_with_single_line(
7526 frontend: &mut FrontendState,
7527 ctx: &ExecutorContext,
7528 mock_ctx: &ExecutorContext,
7529 version: Version,
7530 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7531 frontend.program = Program::empty();
7532
7533 let sketch_args = SketchCtor {
7534 on: Plane::Default(PlaneName::Xy),
7535 };
7536 let (_src_delta, _scene_delta, sketch_id) = frontend
7537 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7538 .await
7539 .unwrap();
7540
7541 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7542 let (source_delta, scene_graph_delta) = frontend
7543 .add_segment(mock_ctx, version, sketch_id, segment, None)
7544 .await
7545 .unwrap();
7546 let line_id = *scene_graph_delta
7547 .new_objects
7548 .last()
7549 .expect("Expected line object id to be created");
7550
7551 (sketch_id, line_id, source_delta, scene_graph_delta)
7552 }
7553
7554 async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7555 frontend.program = program.clone();
7556 let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7557 frontend.update_state_after_exec(outcome, true);
7558 }
7559
7560 #[test]
7561 fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7562 for (source, expected_message) in [
7563 ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7564 (
7565 "3'",
7566 "Error parsing KCL source after editing: unterminated string literal",
7567 ),
7568 ] {
7569 let err = parse_frontend_mutation_source(
7570 source,
7571 "Error parsing KCL source after editing",
7572 "No AST produced after editing",
7573 )
7574 .expect_err("expected invalid KCL source to fail");
7575 let message = err.error.message();
7576
7577 assert_eq!(message, expected_message);
7578 assert!(!message.contains("CompilationIssue"));
7579 assert!(!message.contains("KclErrorDetails"));
7580 assert!(!message.contains("source_range"));
7581 }
7582 }
7583
7584 #[tokio::test(flavor = "multi_thread")]
7585 async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7586 let initial_source = "\
7587sketch(on = XY) {
7588 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7589 distance([line1.start, line1.end]) == 10
7590}
7591";
7592 let program = Program::parse(initial_source).unwrap().0.unwrap();
7593
7594 let mut frontend = FrontendState::new();
7595 let mock_ctx = ExecutorContext::new_mock(None).await;
7596 let version = Version(0);
7597
7598 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7599 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7600 let sketch_id = sketch_object.id;
7601 let sketch = expect_sketch(sketch_object);
7602 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7603
7604 for (value, expected_message) in [
7605 ("**", "Invalid constraint value: Unexpected token: *"),
7606 ("3'", "Invalid constraint value: unterminated string literal"),
7607 ] {
7608 let err = frontend
7609 .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7610 .await
7611 .expect_err("expected invalid constraint expression to fail");
7612 let message = err.error.message();
7613
7614 assert_eq!(message, expected_message);
7615 assert!(!message.contains("CompilationIssue"));
7616 assert!(!message.contains("KclErrorDetails"));
7617 assert!(!message.contains("source_range"));
7618 }
7619
7620 mock_ctx.close().await;
7621 }
7622
7623 #[tokio::test(flavor = "multi_thread")]
7624 async fn test_failed_edit_constraint_value_does_not_update_program() {
7625 let initial_source = "\
7626sketch(on = XY) {
7627 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7628 distance([line1.start, line1.end]) == 10
7629}
7630";
7631 let program = Program::parse(initial_source).unwrap().0.unwrap();
7632 let original_source = program.original_file_contents.clone();
7633
7634 let mut frontend = FrontendState::new();
7635 let mock_ctx = ExecutorContext::new_mock(None).await;
7636 let version = Version(0);
7637
7638 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7639 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7640 let sketch_id = sketch_object.id;
7641 let sketch = expect_sketch(sketch_object);
7642 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7643
7644 frontend
7645 .edit_constraint_value(
7646 &mock_ctx,
7647 version,
7648 sketch_id,
7649 constraint_id,
7650 "unknownDistance".to_owned(),
7651 )
7652 .await
7653 .expect_err("expected invalid constraint value to fail execution");
7654
7655 assert_eq!(frontend.program.original_file_contents, original_source);
7656 assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7657
7658 mock_ctx.close().await;
7659 }
7660
7661 #[tokio::test(flavor = "multi_thread")]
7662 async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7663 let initial_source = "\
7664arr = [0]
7665sketch(on = XY) {
7666 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7667 distance([line1.start, line1.end]) == 10
7668}
7669";
7670 let program = Program::parse(initial_source).unwrap().0.unwrap();
7671
7672 let mut frontend = FrontendState::new();
7673 let mock_ctx = ExecutorContext::new_mock(None).await;
7674 let version = Version(0);
7675
7676 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7677 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7678 let sketch_id = sketch_object.id;
7679 let sketch = expect_sketch(sketch_object);
7680 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7681
7682 let err = frontend
7686 .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7687 .await
7688 .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7689 let message = err.error.message();
7690 assert!(
7691 message.contains("The array doesn't have any item at index 5"),
7692 "unexpected error message: {message}"
7693 );
7694
7695 mock_ctx.close().await;
7696 }
7697
7698 #[tokio::test(flavor = "multi_thread")]
7699 async fn test_sketch_checkpoint_round_trip_restores_state() {
7700 let mut frontend = FrontendState::new();
7701 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7702 let mock_ctx = ExecutorContext::new_mock(None).await;
7703 let version = Version(0);
7704
7705 let (sketch_id, line_id, source_delta, scene_graph_delta) =
7706 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7707
7708 let expected_source = source_delta.text.clone();
7709 let expected_scene_graph = frontend.scene_graph.clone();
7710 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7711 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7712
7713 let checkpoint_id = frontend
7714 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7715 .await
7716 .unwrap();
7717
7718 let edited_segments = vec![ExistingSegmentCtor {
7719 id: line_id,
7720 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7721 }];
7722 let (edited_source, _edited_scene) = frontend
7723 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7724 .await
7725 .unwrap();
7726 assert_ne!(edited_source.text, expected_source);
7727
7728 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7729
7730 assert_eq!(restored.source_delta.text, expected_source);
7731 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7732 assert!(restored.scene_graph_delta.invalidates_ids);
7733 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7734 assert_eq!(frontend.scene_graph, expected_scene_graph);
7735 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7736
7737 ctx.close().await;
7738 }
7739
7740 #[tokio::test(flavor = "multi_thread")]
7741 async fn test_sketch_checkpoints_prune_oldest_entries() {
7742 let mut frontend = FrontendState::new();
7743 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7744 let mock_ctx = ExecutorContext::new_mock(None).await;
7745 let version = Version(0);
7746
7747 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7748 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7749
7750 let mut checkpoint_ids = Vec::new();
7751 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7752 checkpoint_ids.push(
7753 frontend
7754 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7755 .await
7756 .unwrap(),
7757 );
7758 }
7759
7760 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7761 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7762
7763 let oldest_retained = checkpoint_ids[3];
7764 assert_eq!(
7765 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7766 Some(oldest_retained)
7767 );
7768
7769 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7770 assert!(evicted_restore.is_err());
7771 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7772
7773 frontend
7774 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7775 .await
7776 .unwrap();
7777
7778 ctx.close().await;
7779 }
7780
7781 #[tokio::test(flavor = "multi_thread")]
7782 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7783 let mut frontend = FrontendState::new();
7784 let missing_checkpoint = SketchCheckpointId::new(999);
7785
7786 let err = frontend
7787 .restore_sketch_checkpoint(missing_checkpoint)
7788 .await
7789 .expect_err("Expected restore to fail for missing checkpoint");
7790
7791 assert!(err.msg.contains("Sketch checkpoint not found"));
7792 }
7793
7794 #[tokio::test(flavor = "multi_thread")]
7795 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7796 let mut frontend = FrontendState::new();
7797 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7798 let mock_ctx = ExecutorContext::new_mock(None).await;
7799 let version = Version(0);
7800
7801 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7802 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7803
7804 let checkpoint_a = frontend
7805 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7806 .await
7807 .unwrap();
7808 let checkpoint_b = frontend
7809 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7810 .await
7811 .unwrap();
7812 assert_eq!(frontend.sketch_checkpoints.len(), 2);
7813
7814 frontend.clear_sketch_checkpoints();
7815 assert!(frontend.sketch_checkpoints.is_empty());
7816 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7817 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7818
7819 ctx.close().await;
7820 }
7821
7822 #[tokio::test(flavor = "multi_thread")]
7823 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7824 let mut frontend = FrontendState::new();
7825 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7826 let mock_ctx = ExecutorContext::new_mock(None).await;
7827 let version = Version(0);
7828
7829 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7830 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7831 let old_source = source_delta.text.clone();
7832 let old_checkpoint = frontend
7833 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7834 .await
7835 .unwrap();
7836 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7837
7838 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
7839 .unwrap()
7840 .0
7841 .unwrap();
7842
7843 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7844 let SetProgramOutcome::Success {
7845 checkpoint_id: Some(new_checkpoint),
7846 ..
7847 } = result
7848 else {
7849 panic!("Expected Success with a fresh checkpoint baseline");
7850 };
7851
7852 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7853
7854 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7855 assert_eq!(old_restore.source_delta.text, old_source);
7856
7857 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7858 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7859
7860 ctx.close().await;
7861 }
7862
7863 #[tokio::test(flavor = "multi_thread")]
7864 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7865 let mut frontend = FrontendState::new();
7866 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7867 let mock_ctx = ExecutorContext::new_mock(None).await;
7868 let version = Version(0);
7869
7870 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7871 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7872 let old_checkpoint = frontend
7873 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7874 .await
7875 .unwrap();
7876 let checkpoint_count_before = frontend.sketch_checkpoints.len();
7877
7878 let failing_program = Program::parse(
7879 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7880 )
7881 .unwrap()
7882 .0
7883 .unwrap();
7884
7885 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7886 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7887 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7888 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7889
7890 ctx.close().await;
7891 }
7892
7893 #[tokio::test(flavor = "multi_thread")]
7894 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7895 let mut frontend = FrontendState::new();
7896 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7897
7898 let program = Program::parse(
7899 "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",
7900 )
7901 .unwrap()
7902 .0
7903 .unwrap();
7904 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7905 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7906 panic!("Expected successful baseline program execution");
7907 };
7908
7909 clear_mem_cache().await;
7910 assert!(read_old_memory().await.is_none());
7911
7912 let checkpoint_without_mock_memory = frontend
7913 .create_sketch_checkpoint((*exec_outcome).clone())
7914 .await
7915 .unwrap();
7916
7917 write_old_memory(SketchModeState::new_for_tests()).await;
7918 assert!(read_old_memory().await.is_some());
7919
7920 let checkpoint_with_mock_memory = frontend
7921 .create_sketch_checkpoint((*exec_outcome).clone())
7922 .await
7923 .unwrap();
7924
7925 clear_mem_cache().await;
7926 assert!(read_old_memory().await.is_none());
7927
7928 frontend
7929 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7930 .await
7931 .unwrap();
7932 assert!(read_old_memory().await.is_some());
7933
7934 frontend
7935 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7936 .await
7937 .unwrap();
7938 assert!(read_old_memory().await.is_none());
7939
7940 ctx.close().await;
7941 }
7942
7943 #[tokio::test(flavor = "multi_thread")]
7944 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7945 let source = "\
7946sketch(on = XY) {
7947 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7948}
7949
7950bad = missing_name
7951";
7952 let program = Program::parse(source).unwrap().0.unwrap();
7953
7954 let mut frontend = FrontendState::new();
7955
7956 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7957 let mock_ctx = ExecutorContext::new_mock(None).await;
7958 let version = Version(0);
7959 let project_id = ProjectId(0);
7960 let file_id = FileId(0);
7961
7962 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7963 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7964 };
7965
7966 let sketch_id = frontend
7967 .scene_graph
7968 .objects
7969 .iter()
7970 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7971 .expect("Expected sketch object from errored hack_set_program");
7972
7973 frontend
7974 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7975 .await
7976 .unwrap();
7977
7978 ctx.close().await;
7979 mock_ctx.close().await;
7980 }
7981
7982 #[tokio::test(flavor = "multi_thread")]
7983 async fn test_new_sketch_add_point_edit_point() {
7984 let program = Program::empty();
7985
7986 let mut frontend = FrontendState::new();
7987 frontend.program = program;
7988
7989 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7990 let mock_ctx = ExecutorContext::new_mock(None).await;
7991 let version = Version(0);
7992
7993 let sketch_args = SketchCtor {
7994 on: Plane::Default(PlaneName::Xy),
7995 };
7996 let (_src_delta, scene_delta, sketch_id) = frontend
7997 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7998 .await
7999 .unwrap();
8000 assert_eq!(sketch_id, ObjectId(1));
8001 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8002 let sketch_object = &scene_delta.new_graph.objects[1];
8003 assert_eq!(sketch_object.id, ObjectId(1));
8004 assert_eq!(
8005 sketch_object.kind,
8006 ObjectKind::Sketch(Sketch {
8007 args: SketchCtor {
8008 on: Plane::Default(PlaneName::Xy)
8009 },
8010 plane: ObjectId(0),
8011 segments: vec![],
8012 constraints: vec![],
8013 })
8014 );
8015 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8016
8017 let point_ctor = PointCtor {
8018 position: Point2d {
8019 x: Expr::Number(Number {
8020 value: 1.0,
8021 units: NumericSuffix::Inch,
8022 }),
8023 y: Expr::Number(Number {
8024 value: 2.0,
8025 units: NumericSuffix::Inch,
8026 }),
8027 },
8028 };
8029 let segment = SegmentCtor::Point(point_ctor);
8030 let (src_delta, scene_delta) = frontend
8031 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8032 .await
8033 .unwrap();
8034 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8035 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8036 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8037 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8038 assert_eq!(scene_object.id.0, i);
8039 }
8040
8041 let point_id = *scene_delta.new_objects.last().unwrap();
8042
8043 let point_ctor = PointCtor {
8044 position: Point2d {
8045 x: Expr::Number(Number {
8046 value: 3.0,
8047 units: NumericSuffix::Inch,
8048 }),
8049 y: Expr::Number(Number {
8050 value: 4.0,
8051 units: NumericSuffix::Inch,
8052 }),
8053 },
8054 };
8055 let segments = vec![ExistingSegmentCtor {
8056 id: point_id,
8057 ctor: SegmentCtor::Point(point_ctor),
8058 }];
8059 let (src_delta, scene_delta) = frontend
8060 .edit_segments(&mock_ctx, version, sketch_id, segments)
8061 .await
8062 .unwrap();
8063 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8064 assert_eq!(scene_delta.new_objects, vec![]);
8065 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8066
8067 ctx.close().await;
8068 mock_ctx.close().await;
8069 }
8070
8071 #[tokio::test(flavor = "multi_thread")]
8072 async fn test_new_sketch_add_line_edit_line() {
8073 let program = Program::empty();
8074
8075 let mut frontend = FrontendState::new();
8076 frontend.program = program;
8077
8078 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8079 let mock_ctx = ExecutorContext::new_mock(None).await;
8080 let version = Version(0);
8081
8082 let sketch_args = SketchCtor {
8083 on: Plane::Default(PlaneName::Xy),
8084 };
8085 let (_src_delta, scene_delta, sketch_id) = frontend
8086 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8087 .await
8088 .unwrap();
8089 assert_eq!(sketch_id, ObjectId(1));
8090 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8091 let sketch_object = &scene_delta.new_graph.objects[1];
8092 assert_eq!(sketch_object.id, ObjectId(1));
8093 assert_eq!(
8094 sketch_object.kind,
8095 ObjectKind::Sketch(Sketch {
8096 args: SketchCtor {
8097 on: Plane::Default(PlaneName::Xy)
8098 },
8099 plane: ObjectId(0),
8100 segments: vec![],
8101 constraints: vec![],
8102 })
8103 );
8104 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8105
8106 let line_ctor = LineCtor {
8107 start: Point2d {
8108 x: Expr::Number(Number {
8109 value: 0.0,
8110 units: NumericSuffix::Mm,
8111 }),
8112 y: Expr::Number(Number {
8113 value: 0.0,
8114 units: NumericSuffix::Mm,
8115 }),
8116 },
8117 end: Point2d {
8118 x: Expr::Number(Number {
8119 value: 10.0,
8120 units: NumericSuffix::Mm,
8121 }),
8122 y: Expr::Number(Number {
8123 value: 10.0,
8124 units: NumericSuffix::Mm,
8125 }),
8126 },
8127 construction: None,
8128 };
8129 let segment = SegmentCtor::Line(line_ctor);
8130 let (src_delta, scene_delta) = frontend
8131 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8132 .await
8133 .unwrap();
8134 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8135 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8136 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8137 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8138 assert_eq!(scene_object.id.0, i);
8139 }
8140
8141 let line = *scene_delta.new_objects.last().unwrap();
8143
8144 let line_ctor = LineCtor {
8145 start: Point2d {
8146 x: Expr::Number(Number {
8147 value: 1.0,
8148 units: NumericSuffix::Mm,
8149 }),
8150 y: Expr::Number(Number {
8151 value: 2.0,
8152 units: NumericSuffix::Mm,
8153 }),
8154 },
8155 end: Point2d {
8156 x: Expr::Number(Number {
8157 value: 13.0,
8158 units: NumericSuffix::Mm,
8159 }),
8160 y: Expr::Number(Number {
8161 value: 14.0,
8162 units: NumericSuffix::Mm,
8163 }),
8164 },
8165 construction: None,
8166 };
8167 let segments = vec![ExistingSegmentCtor {
8168 id: line,
8169 ctor: SegmentCtor::Line(line_ctor),
8170 }];
8171 let (src_delta, scene_delta) = frontend
8172 .edit_segments(&mock_ctx, version, sketch_id, segments)
8173 .await
8174 .unwrap();
8175 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8176 assert_eq!(scene_delta.new_objects, vec![]);
8177 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8178
8179 ctx.close().await;
8180 mock_ctx.close().await;
8181 }
8182
8183 #[tokio::test(flavor = "multi_thread")]
8184 async fn test_new_sketch_add_arc_edit_arc() {
8185 let program = Program::empty();
8186
8187 let mut frontend = FrontendState::new();
8188 frontend.program = program;
8189
8190 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8191 let mock_ctx = ExecutorContext::new_mock(None).await;
8192 let version = Version(0);
8193
8194 let sketch_args = SketchCtor {
8195 on: Plane::Default(PlaneName::Xy),
8196 };
8197 let (_src_delta, scene_delta, sketch_id) = frontend
8198 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8199 .await
8200 .unwrap();
8201 assert_eq!(sketch_id, ObjectId(1));
8202 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8203 let sketch_object = &scene_delta.new_graph.objects[1];
8204 assert_eq!(sketch_object.id, ObjectId(1));
8205 assert_eq!(
8206 sketch_object.kind,
8207 ObjectKind::Sketch(Sketch {
8208 args: SketchCtor {
8209 on: Plane::Default(PlaneName::Xy),
8210 },
8211 plane: ObjectId(0),
8212 segments: vec![],
8213 constraints: vec![],
8214 })
8215 );
8216 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8217
8218 let arc_ctor = ArcCtor {
8219 start: Point2d {
8220 x: Expr::Var(Number {
8221 value: 0.0,
8222 units: NumericSuffix::Mm,
8223 }),
8224 y: Expr::Var(Number {
8225 value: 0.0,
8226 units: NumericSuffix::Mm,
8227 }),
8228 },
8229 end: Point2d {
8230 x: Expr::Var(Number {
8231 value: 10.0,
8232 units: NumericSuffix::Mm,
8233 }),
8234 y: Expr::Var(Number {
8235 value: 10.0,
8236 units: NumericSuffix::Mm,
8237 }),
8238 },
8239 center: Point2d {
8240 x: Expr::Var(Number {
8241 value: 10.0,
8242 units: NumericSuffix::Mm,
8243 }),
8244 y: Expr::Var(Number {
8245 value: 0.0,
8246 units: NumericSuffix::Mm,
8247 }),
8248 },
8249 direction: None,
8250 construction: None,
8251 };
8252 let segment = SegmentCtor::Arc(arc_ctor);
8253 let (src_delta, scene_delta) = frontend
8254 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8255 .await
8256 .unwrap();
8257 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8258 assert_eq!(
8259 scene_delta.new_objects,
8260 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8261 );
8262 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8263 assert_eq!(scene_object.id.0, i);
8264 }
8265 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8266
8267 let arc = *scene_delta.new_objects.last().unwrap();
8269
8270 let arc_ctor = ArcCtor {
8271 start: Point2d {
8272 x: Expr::Var(Number {
8273 value: 1.0,
8274 units: NumericSuffix::Mm,
8275 }),
8276 y: Expr::Var(Number {
8277 value: 2.0,
8278 units: NumericSuffix::Mm,
8279 }),
8280 },
8281 end: Point2d {
8282 x: Expr::Var(Number {
8283 value: 13.0,
8284 units: NumericSuffix::Mm,
8285 }),
8286 y: Expr::Var(Number {
8287 value: 14.0,
8288 units: NumericSuffix::Mm,
8289 }),
8290 },
8291 center: Point2d {
8292 x: Expr::Var(Number {
8293 value: 13.0,
8294 units: NumericSuffix::Mm,
8295 }),
8296 y: Expr::Var(Number {
8297 value: 2.0,
8298 units: NumericSuffix::Mm,
8299 }),
8300 },
8301 direction: None,
8302 construction: None,
8303 };
8304 let segments = vec![ExistingSegmentCtor {
8305 id: arc,
8306 ctor: SegmentCtor::Arc(arc_ctor),
8307 }];
8308 let (src_delta, scene_delta) = frontend
8309 .edit_segments(&mock_ctx, version, sketch_id, segments)
8310 .await
8311 .unwrap();
8312 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8313 assert_eq!(scene_delta.new_objects, vec![]);
8314 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8315
8316 ctx.close().await;
8317 mock_ctx.close().await;
8318 }
8319
8320 #[tokio::test(flavor = "multi_thread")]
8321 async fn test_new_sketch_add_circle_edit_circle() {
8322 let program = Program::empty();
8323
8324 let mut frontend = FrontendState::new();
8325 frontend.program = program;
8326
8327 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8328 let mock_ctx = ExecutorContext::new_mock(None).await;
8329 let version = Version(0);
8330
8331 let sketch_args = SketchCtor {
8332 on: Plane::Default(PlaneName::Xy),
8333 };
8334 let (_src_delta, _scene_delta, sketch_id) = frontend
8335 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8336 .await
8337 .unwrap();
8338
8339 let circle_ctor = CircleCtor {
8341 start: Point2d {
8342 x: Expr::Var(Number {
8343 value: 5.0,
8344 units: NumericSuffix::Mm,
8345 }),
8346 y: Expr::Var(Number {
8347 value: 0.0,
8348 units: NumericSuffix::Mm,
8349 }),
8350 },
8351 center: Point2d {
8352 x: Expr::Var(Number {
8353 value: 0.0,
8354 units: NumericSuffix::Mm,
8355 }),
8356 y: Expr::Var(Number {
8357 value: 0.0,
8358 units: NumericSuffix::Mm,
8359 }),
8360 },
8361 construction: None,
8362 };
8363 let segment = SegmentCtor::Circle(circle_ctor);
8364 let (src_delta, scene_delta) = frontend
8365 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8366 .await
8367 .unwrap();
8368 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8369 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8371 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8372
8373 let circle = *scene_delta.new_objects.last().unwrap();
8374
8375 let circle_ctor = CircleCtor {
8377 start: Point2d {
8378 x: Expr::Var(Number {
8379 value: 10.0,
8380 units: NumericSuffix::Mm,
8381 }),
8382 y: Expr::Var(Number {
8383 value: 0.0,
8384 units: NumericSuffix::Mm,
8385 }),
8386 },
8387 center: Point2d {
8388 x: Expr::Var(Number {
8389 value: 3.0,
8390 units: NumericSuffix::Mm,
8391 }),
8392 y: Expr::Var(Number {
8393 value: 4.0,
8394 units: NumericSuffix::Mm,
8395 }),
8396 },
8397 construction: None,
8398 };
8399 let segments = vec![ExistingSegmentCtor {
8400 id: circle,
8401 ctor: SegmentCtor::Circle(circle_ctor),
8402 }];
8403 let (src_delta, scene_delta) = frontend
8404 .edit_segments(&mock_ctx, version, sketch_id, segments)
8405 .await
8406 .unwrap();
8407 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8408 assert_eq!(scene_delta.new_objects, vec![]);
8409 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8410
8411 ctx.close().await;
8412 mock_ctx.close().await;
8413 }
8414
8415 #[tokio::test(flavor = "multi_thread")]
8416 async fn test_delete_circle() {
8417 let initial_source = "sketch001 = sketch(on = XY) {
8418 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8419}
8420";
8421
8422 let program = Program::parse(initial_source).unwrap().0.unwrap();
8423 let mut frontend = FrontendState::new();
8424
8425 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8426 let mock_ctx = ExecutorContext::new_mock(None).await;
8427 let version = Version(0);
8428
8429 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8430 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8431 let sketch_id = sketch_object.id;
8432 let sketch = expect_sketch(sketch_object);
8433
8434 assert_eq!(sketch.segments.len(), 3);
8436 let circle_id = sketch.segments[2];
8437
8438 let (src_delta, scene_delta) = frontend
8440 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8441 .await
8442 .unwrap();
8443 insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8444 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8445 let new_sketch = expect_sketch(new_sketch_object);
8446 assert_eq!(new_sketch.segments.len(), 0);
8447
8448 ctx.close().await;
8449 mock_ctx.close().await;
8450 }
8451
8452 #[tokio::test(flavor = "multi_thread")]
8453 async fn test_edit_circle_via_point() {
8454 let initial_source = "sketch001 = sketch(on = XY) {
8455 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8456}
8457";
8458
8459 let program = Program::parse(initial_source).unwrap().0.unwrap();
8460 let mut frontend = FrontendState::new();
8461
8462 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8463 let mock_ctx = ExecutorContext::new_mock(None).await;
8464 let version = Version(0);
8465
8466 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8467 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8468 let sketch_id = sketch_object.id;
8469 let sketch = expect_sketch(sketch_object);
8470
8471 let circle_id = sketch
8473 .segments
8474 .iter()
8475 .copied()
8476 .find(|seg_id| {
8477 matches!(
8478 &frontend.scene_graph.objects[seg_id.0].kind,
8479 ObjectKind::Segment {
8480 segment: Segment::Circle(_)
8481 }
8482 )
8483 })
8484 .expect("Expected a circle segment in sketch");
8485 let circle_object = &frontend.scene_graph.objects[circle_id.0];
8486 let ObjectKind::Segment {
8487 segment: Segment::Circle(circle),
8488 } = &circle_object.kind
8489 else {
8490 panic!("Expected circle segment, got: {:?}", circle_object.kind);
8491 };
8492 let start_point_id = circle.start;
8493
8494 let segments = vec![ExistingSegmentCtor {
8496 id: start_point_id,
8497 ctor: SegmentCtor::Point(PointCtor {
8498 position: Point2d {
8499 x: Expr::Var(Number {
8500 value: 7.0,
8501 units: NumericSuffix::Mm,
8502 }),
8503 y: Expr::Var(Number {
8504 value: 1.0,
8505 units: NumericSuffix::Mm,
8506 }),
8507 },
8508 }),
8509 }];
8510 let (src_delta, _scene_delta) = frontend
8511 .edit_segments(&mock_ctx, version, sketch_id, segments)
8512 .await
8513 .unwrap();
8514 insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8515
8516 ctx.close().await;
8517 mock_ctx.close().await;
8518 }
8519
8520 #[tokio::test(flavor = "multi_thread")]
8521 async fn test_add_line_when_sketch_block_uses_variable() {
8522 let initial_source = "s = sketch(on = XY) {}
8523";
8524
8525 let program = Program::parse(initial_source).unwrap().0.unwrap();
8526
8527 let mut frontend = FrontendState::new();
8528
8529 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8530 let mock_ctx = ExecutorContext::new_mock(None).await;
8531 let version = Version(0);
8532
8533 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8534 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8535 let sketch_id = sketch_object.id;
8536
8537 let line_ctor = LineCtor {
8538 start: Point2d {
8539 x: Expr::Number(Number {
8540 value: 0.0,
8541 units: NumericSuffix::Mm,
8542 }),
8543 y: Expr::Number(Number {
8544 value: 0.0,
8545 units: NumericSuffix::Mm,
8546 }),
8547 },
8548 end: Point2d {
8549 x: Expr::Number(Number {
8550 value: 10.0,
8551 units: NumericSuffix::Mm,
8552 }),
8553 y: Expr::Number(Number {
8554 value: 10.0,
8555 units: NumericSuffix::Mm,
8556 }),
8557 },
8558 construction: None,
8559 };
8560 let segment = SegmentCtor::Line(line_ctor);
8561 let (src_delta, scene_delta) = frontend
8562 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8563 .await
8564 .unwrap();
8565 insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8566 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8567 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8568
8569 ctx.close().await;
8570 mock_ctx.close().await;
8571 }
8572
8573 #[tokio::test(flavor = "multi_thread")]
8574 async fn test_new_sketch_add_line_delete_sketch() {
8575 let program = Program::empty();
8576
8577 let mut frontend = FrontendState::new();
8578 frontend.program = program;
8579
8580 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8581 let mock_ctx = ExecutorContext::new_mock(None).await;
8582 let version = Version(0);
8583
8584 let sketch_args = SketchCtor {
8585 on: Plane::Default(PlaneName::Xy),
8586 };
8587 let (_src_delta, scene_delta, sketch_id) = frontend
8588 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8589 .await
8590 .unwrap();
8591 assert_eq!(sketch_id, ObjectId(1));
8592 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8593 let sketch_object = &scene_delta.new_graph.objects[1];
8594 assert_eq!(sketch_object.id, ObjectId(1));
8595 assert_eq!(
8596 sketch_object.kind,
8597 ObjectKind::Sketch(Sketch {
8598 args: SketchCtor {
8599 on: Plane::Default(PlaneName::Xy)
8600 },
8601 plane: ObjectId(0),
8602 segments: vec![],
8603 constraints: vec![],
8604 })
8605 );
8606 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8607
8608 let line_ctor = LineCtor {
8609 start: Point2d {
8610 x: Expr::Number(Number {
8611 value: 0.0,
8612 units: NumericSuffix::Mm,
8613 }),
8614 y: Expr::Number(Number {
8615 value: 0.0,
8616 units: NumericSuffix::Mm,
8617 }),
8618 },
8619 end: Point2d {
8620 x: Expr::Number(Number {
8621 value: 10.0,
8622 units: NumericSuffix::Mm,
8623 }),
8624 y: Expr::Number(Number {
8625 value: 10.0,
8626 units: NumericSuffix::Mm,
8627 }),
8628 },
8629 construction: None,
8630 };
8631 let segment = SegmentCtor::Line(line_ctor);
8632 let (src_delta, scene_delta) = frontend
8633 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8634 .await
8635 .unwrap();
8636 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8637 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8638
8639 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8640 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8641 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8642
8643 ctx.close().await;
8644 mock_ctx.close().await;
8645 }
8646
8647 #[tokio::test(flavor = "multi_thread")]
8648 async fn test_delete_sketch_when_sketch_block_uses_variable() {
8649 let initial_source = "s = sketch(on = XY) {}
8650";
8651
8652 let program = Program::parse(initial_source).unwrap().0.unwrap();
8653
8654 let mut frontend = FrontendState::new();
8655
8656 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8657 let version = Version(0);
8658
8659 frontend.hack_set_program(&ctx, program).await.unwrap();
8660 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8661 let sketch_id = sketch_object.id;
8662
8663 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8664 insta::assert_snapshot!(
8665 "test_delete_sketch_when_sketch_block_uses_variable",
8666 src_delta.text.as_str()
8667 );
8668 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8669
8670 ctx.close().await;
8671 }
8672
8673 #[tokio::test(flavor = "multi_thread")]
8674 async fn test_delete_sketch_after_comment() {
8675 let initial_source = "sketch001 = sketch(on = XZ) {
8676}
8677";
8678
8679 let program = Program::parse(initial_source).unwrap().0.unwrap();
8680 let mut frontend = FrontendState::new();
8681
8682 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8683 let version = Version(0);
8684
8685 frontend.hack_set_program(&ctx, program).await.unwrap();
8686 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8687 let sketch_id = sketch_object.id;
8688 let original_source = sketch_object.source.clone();
8689
8690 let commented_source = "// test 1
8691sketch001 = sketch(on = XZ) {
8692}
8693";
8694 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8695 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8696
8697 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8698 assert_eq!(cached_sketch_object.source, original_source);
8699
8700 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8701 assert!(
8702 !src_delta.text.contains("sketch001"),
8703 "sketch was not deleted: {}",
8704 src_delta.text
8705 );
8706 insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8708 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8709
8710 ctx.close().await;
8711 }
8712
8713 #[tokio::test(flavor = "multi_thread")]
8714 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8715 let initial_source = "sketch001 = sketch(on = XZ) {
8716}
8717foo = 1
8718";
8719
8720 let program = Program::parse(initial_source).unwrap().0.unwrap();
8721 let mut frontend = FrontendState::new();
8722
8723 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8724 let version = Version(0);
8725
8726 frontend.hack_set_program(&ctx, program).await.unwrap();
8727 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8728 let sketch_id = sketch_object.id;
8729
8730 let commented_source = "// keep me
8731sketch001 = sketch(on = XZ) {
8732}
8733foo = 1
8734";
8735 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8736 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8737
8738 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8739 insta::assert_snapshot!(
8741 "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8742 src_delta.text.as_str()
8743 );
8744
8745 ctx.close().await;
8746 }
8747
8748 #[tokio::test(flavor = "multi_thread")]
8749 async fn test_delete_segment_preserves_pre_comment() {
8750 let initial_source = "\
8751sketch(on = XY) {
8752 point(at = [var 1, var 2])
8753 // describe the middle point
8754 point(at = [var 3, var 4])
8755 point(at = [var 5, var 6])
8756}
8757";
8758
8759 let program = Program::parse(initial_source).unwrap().0.unwrap();
8760 let mut frontend = FrontendState::new();
8761
8762 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8763 let mock_ctx = ExecutorContext::new_mock(None).await;
8764 let version = Version(0);
8765
8766 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8767 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8768 let sketch_id = sketch_object.id;
8769 let sketch = expect_sketch(sketch_object);
8770
8771 let middle_point_id = *sketch.segments.get(1).unwrap();
8772
8773 let (src_delta, _scene_delta) = frontend
8774 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8775 .await
8776 .unwrap();
8777 insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8780
8781 ctx.close().await;
8782 mock_ctx.close().await;
8783 }
8784
8785 #[tokio::test(flavor = "multi_thread")]
8786 async fn test_delete_last_segment_preserves_pre_comment() {
8787 let initial_source = "\
8788sketch(on = XY) {
8789 point(at = [var 1, var 2])
8790 // describe the trailing point
8791 point(at = [var 3, var 4])
8792}
8793";
8794
8795 let program = Program::parse(initial_source).unwrap().0.unwrap();
8796 let mut frontend = FrontendState::new();
8797
8798 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8799 let mock_ctx = ExecutorContext::new_mock(None).await;
8800 let version = Version(0);
8801
8802 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8803 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8804 let sketch_id = sketch_object.id;
8805 let sketch = expect_sketch(sketch_object);
8806
8807 let last_point_id = *sketch.segments.last().unwrap();
8808
8809 let (src_delta, _scene_delta) = frontend
8810 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8811 .await
8812 .unwrap();
8813 insta::assert_snapshot!(
8816 "test_delete_last_segment_preserves_pre_comment",
8817 src_delta.text.as_str()
8818 );
8819
8820 ctx.close().await;
8821 mock_ctx.close().await;
8822 }
8823
8824 #[tokio::test(flavor = "multi_thread")]
8825 async fn test_delete_segment_drops_inline_trailing_comment() {
8826 let initial_source = "\
8827sketch(on = XY) {
8828 point(at = [var 1, var 2])
8829 point(at = [var 3, var 4]) // same-line note that gets dropped
8830 point(at = [var 5, var 6])
8831}
8832";
8833
8834 let program = Program::parse(initial_source).unwrap().0.unwrap();
8835 let mut frontend = FrontendState::new();
8836
8837 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8838 let mock_ctx = ExecutorContext::new_mock(None).await;
8839 let version = Version(0);
8840
8841 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8842 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8843 let sketch_id = sketch_object.id;
8844 let sketch = expect_sketch(sketch_object);
8845
8846 let middle_point_id = *sketch.segments.get(1).unwrap();
8847
8848 let (src_delta, _scene_delta) = frontend
8849 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8850 .await
8851 .unwrap();
8852 assert!(
8854 !src_delta.text.contains("same-line note"),
8855 "inline comment should have been removed: {}",
8856 src_delta.text
8857 );
8858
8859 ctx.close().await;
8860 mock_ctx.close().await;
8861 }
8862
8863 #[tokio::test(flavor = "multi_thread")]
8864 async fn test_delete_segments_preserves_block_comments_across_positions() {
8865 let initial_source = "\
8873sketch(on = XY) {
8874 /* above first - moves to middle */
8875 point(at = [var 1, var 2]) /* same-line on first - dropped */
8876 /* above middle - stays */
8877 point(at = [var 3, var 4])
8878 /* above last - moves to trailing meta */
8879 point(at = [var 5, var 6])
8880}
8881";
8882
8883 let program = Program::parse(initial_source).unwrap().0.unwrap();
8884 let mut frontend = FrontendState::new();
8885
8886 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8887 let mock_ctx = ExecutorContext::new_mock(None).await;
8888 let version = Version(0);
8889
8890 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8891 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8892 let sketch_id = sketch_object.id;
8893 let sketch = expect_sketch(sketch_object);
8894
8895 let first_point_id = *sketch.segments.first().unwrap();
8896 let last_point_id = *sketch.segments.last().unwrap();
8897
8898 let (src_delta, _scene_delta) = frontend
8899 .delete_objects(
8900 &mock_ctx,
8901 version,
8902 sketch_id,
8903 Vec::new(),
8904 vec![first_point_id, last_point_id],
8905 )
8906 .await
8907 .unwrap();
8908 insta::assert_snapshot!(
8909 "test_delete_segments_preserves_block_comments_across_positions",
8910 src_delta.text.as_str()
8911 );
8912
8913 ctx.close().await;
8914 mock_ctx.close().await;
8915 }
8916
8917 #[tokio::test(flavor = "multi_thread")]
8918 async fn test_edit_line_when_editing_its_start_point() {
8919 let initial_source = "\
8920sketch(on = XY) {
8921 line(start = [var 1, var 2], end = [var 3, var 4])
8922}
8923";
8924
8925 let program = Program::parse(initial_source).unwrap().0.unwrap();
8926
8927 let mut frontend = FrontendState::new();
8928
8929 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8930 let mock_ctx = ExecutorContext::new_mock(None).await;
8931 let version = Version(0);
8932
8933 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8934 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8935 let sketch_id = sketch_object.id;
8936 let sketch = expect_sketch(sketch_object);
8937
8938 let point_id = *sketch.segments.first().unwrap();
8939
8940 let point_ctor = PointCtor {
8941 position: Point2d {
8942 x: Expr::Var(Number {
8943 value: 5.0,
8944 units: NumericSuffix::Inch,
8945 }),
8946 y: Expr::Var(Number {
8947 value: 6.0,
8948 units: NumericSuffix::Inch,
8949 }),
8950 },
8951 };
8952 let segments = vec![ExistingSegmentCtor {
8953 id: point_id,
8954 ctor: SegmentCtor::Point(point_ctor),
8955 }];
8956 let (src_delta, scene_delta) = frontend
8957 .edit_segments(&mock_ctx, version, sketch_id, segments)
8958 .await
8959 .unwrap();
8960 insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
8961 assert_eq!(scene_delta.new_objects, vec![]);
8962 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8963
8964 ctx.close().await;
8965 mock_ctx.close().await;
8966 }
8967
8968 #[tokio::test(flavor = "multi_thread")]
8969 async fn test_edit_line_when_editing_its_end_point() {
8970 let initial_source = "\
8971sketch(on = XY) {
8972 line(start = [var 1, var 2], end = [var 3, var 4])
8973}
8974";
8975
8976 let program = Program::parse(initial_source).unwrap().0.unwrap();
8977
8978 let mut frontend = FrontendState::new();
8979
8980 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8981 let mock_ctx = ExecutorContext::new_mock(None).await;
8982 let version = Version(0);
8983
8984 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8985 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8986 let sketch_id = sketch_object.id;
8987 let sketch = expect_sketch(sketch_object);
8988 let point_id = *sketch.segments.get(1).unwrap();
8989
8990 let point_ctor = PointCtor {
8991 position: Point2d {
8992 x: Expr::Var(Number {
8993 value: 5.0,
8994 units: NumericSuffix::Inch,
8995 }),
8996 y: Expr::Var(Number {
8997 value: 6.0,
8998 units: NumericSuffix::Inch,
8999 }),
9000 },
9001 };
9002 let segments = vec![ExistingSegmentCtor {
9003 id: point_id,
9004 ctor: SegmentCtor::Point(point_ctor),
9005 }];
9006 let (src_delta, scene_delta) = frontend
9007 .edit_segments(&mock_ctx, version, sketch_id, segments)
9008 .await
9009 .unwrap();
9010 insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9011 assert_eq!(scene_delta.new_objects, vec![]);
9012 assert_eq!(
9013 scene_delta.new_graph.objects.len(),
9014 5,
9015 "{:#?}",
9016 scene_delta.new_graph.objects
9017 );
9018
9019 ctx.close().await;
9020 mock_ctx.close().await;
9021 }
9022
9023 #[tokio::test(flavor = "multi_thread")]
9024 async fn test_edit_line_with_coincident_feedback() {
9025 let initial_source = "\
9026sketch(on = XY) {
9027 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9028 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9029 fixed([line1.start, [0, 0]])
9030 coincident([line1.end, line2.start])
9031 equalLength([line1, line2])
9032}
9033";
9034
9035 let program = Program::parse(initial_source).unwrap().0.unwrap();
9036
9037 let mut frontend = FrontendState::new();
9038
9039 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9040 let mock_ctx = ExecutorContext::new_mock(None).await;
9041 let version = Version(0);
9042
9043 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9044 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9045 let sketch_id = sketch_object.id;
9046 let sketch = expect_sketch(sketch_object);
9047 let line2_end_id = *sketch.segments.get(4).unwrap();
9048
9049 let segments = vec![ExistingSegmentCtor {
9050 id: line2_end_id,
9051 ctor: SegmentCtor::Point(PointCtor {
9052 position: Point2d {
9053 x: Expr::Var(Number {
9054 value: 9.0,
9055 units: NumericSuffix::None,
9056 }),
9057 y: Expr::Var(Number {
9058 value: 10.0,
9059 units: NumericSuffix::None,
9060 }),
9061 },
9062 }),
9063 }];
9064 let (src_delta, scene_delta) = frontend
9065 .edit_segments(&mock_ctx, version, sketch_id, segments)
9066 .await
9067 .unwrap();
9068 insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9069 assert_eq!(
9070 scene_delta.new_graph.objects.len(),
9071 11,
9072 "{:#?}",
9073 scene_delta.new_graph.objects
9074 );
9075
9076 ctx.close().await;
9077 mock_ctx.close().await;
9078 }
9079
9080 #[tokio::test(flavor = "multi_thread")]
9081 async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9082 let initial_source = "\
9083sketch(on = XY) {
9084 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9085 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9086 fixed([line1.start, [0, 0]])
9087 coincident([line1.end, line2.start])
9088 equalLength([line1, line2])
9089}
9090";
9091
9092 let program = Program::parse(initial_source).unwrap().0.unwrap();
9093 let mut frontend = FrontendState::new();
9094 let mock_ctx = ExecutorContext::new_mock(None).await;
9095 let version = Version(0);
9096
9097 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9098 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9099 let sketch_id = sketch_object.id;
9100 let sketch = expect_sketch(sketch_object);
9101 let line2_end_id = *sketch.segments.get(4).unwrap();
9102
9103 let segments = vec![ExistingSegmentCtor {
9104 id: line2_end_id,
9105 ctor: SegmentCtor::Point(PointCtor {
9106 position: Point2d {
9107 x: Expr::Var(Number {
9108 value: 9.0,
9109 units: NumericSuffix::None,
9110 }),
9111 y: Expr::Var(Number {
9112 value: 10.0,
9113 units: NumericSuffix::None,
9114 }),
9115 },
9116 }),
9117 }];
9118 let (edited_source, _) = frontend
9119 .edit_segments(&mock_ctx, version, sketch_id, segments)
9120 .await
9121 .unwrap();
9122
9123 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9124 assert_eq!(mock_source.text, edited_source.text);
9125
9126 mock_ctx.close().await;
9127 }
9128
9129 #[tokio::test(flavor = "multi_thread")]
9132 async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9133 let initial_source = "\
9134sketch(on = XY) {
9135 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9136 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9137 fixed([line1.start, [0, 0]])
9138 coincident([line1.end, line2.start])
9139 equalLength([line1, line2])
9140}
9141";
9142
9143 let program = Program::parse(initial_source).unwrap().0.unwrap();
9144 let mut frontend = FrontendState::new();
9145 let mock_ctx = ExecutorContext::new_mock(None).await;
9146 let version = Version(0);
9147
9148 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9149 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9150 let sketch_id = sketch_object.id;
9151 let sketch = expect_sketch(sketch_object);
9152 let line2_end_id = *sketch.segments.get(4).unwrap();
9153
9154 let segments = vec![ExistingSegmentCtor {
9155 id: line2_end_id,
9156 ctor: SegmentCtor::Point(PointCtor {
9157 position: Point2d {
9158 x: Expr::Var(Number {
9159 value: 9.0,
9160 units: NumericSuffix::None,
9161 }),
9162 y: Expr::Var(Number {
9163 value: 10.0,
9164 units: NumericSuffix::None,
9165 }),
9166 },
9167 }),
9168 }];
9169 let (preview_source, preview_delta) = frontend
9170 .edit_segments_with_options(
9171 &mock_ctx,
9172 version,
9173 sketch_id,
9174 segments,
9175 EditSegmentsOptions {
9176 anchor_segment_ids: Some(vec![line2_end_id]),
9177 drag_anchors: Vec::new(),
9178 constraint_label_edits: Vec::new(),
9179 commit_solved_initial_guesses: false,
9180 },
9181 )
9182 .await
9183 .unwrap();
9184
9185 assert!(
9186 !preview_delta.exec_outcome.var_solutions.is_empty(),
9187 "preview solve should still solve and return geometry feedback"
9188 );
9189 assert!(
9190 preview_source
9191 .text
9192 .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9193 );
9194 assert!(
9195 preview_source
9196 .text
9197 .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9198 );
9199
9200 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9201 assert_eq!(mock_source.text, preview_source.text);
9202
9203 mock_ctx.close().await;
9204 }
9205
9206 #[tokio::test(flavor = "multi_thread")]
9207 async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9208 let initial_source = "\
9209sketch(on = XY) {
9210 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9211}
9212";
9213
9214 let program = Program::parse(initial_source).unwrap().0.unwrap();
9215 let mut frontend = FrontendState::new();
9216 let mock_ctx = ExecutorContext::new_mock(None).await;
9217 let version = Version(0);
9218
9219 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9220 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9221 let sketch_id = sketch_object.id;
9222 let sketch = expect_sketch(sketch_object);
9223 let line_end_id = *sketch.segments.get(1).unwrap();
9224
9225 let constraint = Constraint::Fixed(Fixed {
9226 points: vec![FixedPoint {
9227 point: line_end_id,
9228 position: Point2d {
9229 x: Number {
9230 value: 20.0,
9231 units: NumericSuffix::Mm,
9232 },
9233 y: Number {
9234 value: 0.0,
9235 units: NumericSuffix::Mm,
9236 },
9237 },
9238 }],
9239 });
9240 let (constraint_source, _) = frontend
9241 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9242 .await
9243 .unwrap();
9244
9245 assert!(
9246 constraint_source
9247 .text
9248 .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9249 "{}",
9250 constraint_source.text
9251 );
9252 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9253 assert_eq!(mock_source.text, constraint_source.text);
9254
9255 mock_ctx.close().await;
9256 }
9257
9258 #[test]
9259 fn test_no_solver_feedback_preserves_original_source() {
9260 let initial_source = "\
9261@settings(defaultLengthUnit = in, kclVersion = 2.0)
9262cylinder = startSketchOn(XY)
9263 |> circle(center= [0, 0], radius= 22)
9264 |> extrude(length = 14)
9265";
9266 let mut frontend = FrontendState::new();
9267 frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9268 let outcome = ExecOutcome {
9269 variables: Default::default(),
9270 test_program_memory: Default::default(),
9271 operations: Default::default(),
9272 artifact_graph: Default::default(),
9273 scene_objects: Default::default(),
9274 source_range_to_object: Default::default(),
9275 var_solutions: Default::default(),
9276 refactor_metadata: Default::default(),
9277 issues: Default::default(),
9278 filenames: Default::default(),
9279 source_files: Default::default(),
9280 default_planes: Default::default(),
9281 };
9282
9283 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9284
9285 assert_eq!(source_delta.text, initial_source);
9286 }
9287
9288 #[tokio::test(flavor = "multi_thread")]
9291 async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9292 let initial_source = "\
9293sketch(on = XY) {
9294 point1 = point(at = [var 0mm, var 0mm])
9295 point2 = point(at = [var 0mm, var 0mm])
9296 coincident([point1, point2])
9297}
9298";
9299
9300 let program = Program::parse(initial_source).unwrap().0.unwrap();
9301 let mut frontend = FrontendState::new();
9302 let mock_ctx = ExecutorContext::new_mock(None).await;
9303 let version = Version(0);
9304
9305 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9306 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9307 let sketch_id = sketch_object.id;
9308 let sketch = expect_sketch(sketch_object);
9309 let point1_id = sketch.segments[0];
9310 let point2_id = sketch.segments[1];
9311
9312 let segments = vec![
9313 ExistingSegmentCtor {
9314 id: point1_id,
9315 ctor: SegmentCtor::Point(PointCtor {
9316 position: point_expr_mm(10.0, 0.0),
9317 }),
9318 },
9319 ExistingSegmentCtor {
9320 id: point2_id,
9321 ctor: SegmentCtor::Point(PointCtor {
9322 position: point_expr_mm(100.0, 0.0),
9323 }),
9324 },
9325 ];
9326 let (_, scene_delta) = frontend
9327 .edit_segments_with_options(
9328 &mock_ctx,
9329 version,
9330 sketch_id,
9331 segments,
9332 EditSegmentsOptions {
9333 anchor_segment_ids: Some(vec![point1_id]),
9334 drag_anchors: Vec::new(),
9335 constraint_label_edits: Vec::new(),
9336 commit_solved_initial_guesses: true,
9337 },
9338 )
9339 .await
9340 .unwrap();
9341
9342 assert_point_position_close(
9343 point_position(&scene_delta.new_graph, point1_id),
9344 point_number_mm(10.0, 0.0),
9345 );
9346 assert_point_position_close(
9347 point_position(&scene_delta.new_graph, point2_id),
9348 point_number_mm(10.0, 0.0),
9349 );
9350
9351 mock_ctx.close().await;
9352 }
9353
9354 fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9357 use std::cell::RefCell;
9358 struct Collector {
9359 target: f64,
9360 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9361 }
9362 impl<'a> crate::walk::Visitor<'a> for &Collector {
9363 type Error = crate::front::Error;
9364 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9365 if let crate::walk::Node::SketchVar(sketch_var) = node
9366 && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9367 && (initial.value - self.target).abs() < 1e-9
9368 {
9369 self.out
9370 .borrow_mut()
9371 .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9372 }
9373 for child in node.children().iter() {
9374 if !child.visit(*self)? {
9375 return Ok(false);
9376 }
9377 }
9378 Ok(true)
9379 }
9380 }
9381 let collector = Collector {
9382 target: value,
9383 out: Default::default(),
9384 };
9385 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9386 collector.out.into_inner()
9387 }
9388
9389 fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9392 use std::cell::RefCell;
9393 struct Collector {
9394 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9395 }
9396 impl<'a> crate::walk::Visitor<'a> for &Collector {
9397 type Error = crate::front::Error;
9398 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9399 if let crate::walk::Node::SketchVar(sketch_var) = node
9400 && let Some(node_path) = &sketch_var.node_path
9401 {
9402 self.out
9403 .borrow_mut()
9404 .push((SourceRange::from(sketch_var), node_path.clone()));
9405 }
9406 for child in node.children().iter() {
9407 if !child.visit(*self)? {
9408 return Ok(false);
9409 }
9410 }
9411 Ok(true)
9412 }
9413 }
9414 let collector = Collector {
9415 out: Default::default(),
9416 };
9417 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9418 collector.out.into_inner()
9419 }
9420
9421 fn empty_exec_outcome_with_var_solutions(
9422 var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9423 ) -> ExecOutcome {
9424 ExecOutcome {
9425 variables: Default::default(),
9426 test_program_memory: Default::default(),
9427 operations: Default::default(),
9428 artifact_graph: Default::default(),
9429 scene_objects: Default::default(),
9430 source_range_to_object: Default::default(),
9431 var_solutions,
9432 refactor_metadata: Default::default(),
9433 issues: Default::default(),
9434 filenames: Default::default(),
9435 source_files: Default::default(),
9436 default_planes: Default::default(),
9437 }
9438 }
9439
9440 #[test]
9444 fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9445 let initial_source = "\
9446sketch(on = XY) {
9447 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9448}
9449";
9450 let program = Program::parse(initial_source).unwrap().0.unwrap();
9451 let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9452 assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9453 let (literal_range, node_path) = matches.into_iter().next().unwrap();
9454
9455 let mut frontend = FrontendState::new();
9456 frontend.program = program;
9457
9458 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9459 literal_range,
9460 Some(node_path),
9461 Number {
9462 value: 25.0,
9463 units: NumericSuffix::Mm,
9464 },
9465 )]);
9466
9467 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9468
9469 insta::assert_snapshot!(
9470 "test_commit_var_solution_by_node_path_updates_sketch_var",
9471 source_delta.text
9472 );
9473 }
9474
9475 #[test]
9482 fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9483 let compact_source = "\
9484sketch(on = XY) {
9485 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9486}
9487";
9488 let padded_source = "\
9489// added comment\n// added comment\n\nsketch(on = XY) {
9490 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9491}
9492";
9493 let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9494 let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9495
9496 let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9497 .into_iter()
9498 .next()
9499 .expect("expected `var 10mm` in compact source");
9500 let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9501 .into_iter()
9502 .next()
9503 .expect("expected `var 10mm` in padded source");
9504
9505 assert_ne!(
9506 compact_match.0, padded_match.0,
9507 "byte offsets must differ for this test to be meaningful"
9508 );
9509 assert_eq!(
9510 compact_match.1, padded_match.1,
9511 "node paths must agree across whitespace; that's the whole point of NodePath",
9512 );
9513
9514 let mut frontend = FrontendState::new();
9515 frontend.program = padded_program;
9516
9517 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9519 compact_match.0,
9520 Some(compact_match.1),
9521 Number {
9522 value: 30.0,
9523 units: NumericSuffix::Mm,
9524 },
9525 )]);
9526
9527 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9528
9529 insta::assert_snapshot!(
9530 "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9531 source_delta.text
9532 );
9533 }
9534
9535 #[test]
9539 fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9540 let initial_source = "\
9541sketch(on = XY) {
9542 line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9543}
9544";
9545 let program = Program::parse(initial_source).unwrap().0.unwrap();
9546
9547 let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9548 .into_iter()
9549 .next()
9550 .expect("expected `var 10mm`");
9551 let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9552 .into_iter()
9553 .next()
9554 .expect("expected `var 20mm`");
9555
9556 let mut frontend = FrontendState::new();
9557 frontend.program = program;
9558
9559 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9561 var_20.0,
9562 Some(var_10.1),
9563 Number {
9564 value: 33.0,
9565 units: NumericSuffix::Mm,
9566 },
9567 )]);
9568
9569 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9570
9571 insta::assert_snapshot!(
9572 "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9573 source_delta.text
9574 );
9575 }
9576
9577 #[test]
9584 fn test_commit_var_solution_writes_back_into_bare_var() {
9585 let initial_source = "\
9586@settings(experimentalFeatures = allow, kclVersion = 2.0)
9587sketch(on = XY) {
9588 line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9589}
9590";
9591 let program = Program::parse(initial_source).unwrap().0.unwrap();
9592
9593 let bare = collect_all_sketch_vars(&program)
9596 .into_iter()
9597 .find(|(range, _)| {
9598 range.end() - range.start() == 3
9600 })
9601 .expect("expected at least one bare `var`");
9602
9603 let mut frontend = FrontendState::new();
9604 frontend.program = program;
9605
9606 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9607 bare.0,
9608 Some(bare.1),
9609 Number {
9610 value: 7.0,
9611 units: NumericSuffix::Mm,
9612 },
9613 )]);
9614
9615 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9616
9617 insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9621 }
9622
9623 #[tokio::test(flavor = "multi_thread")]
9624 async fn test_delete_point_without_var() {
9625 let initial_source = "\
9626sketch(on = XY) {
9627 point(at = [var 1, var 2])
9628 point(at = [var 3, var 4])
9629 point(at = [var 5, var 6])
9630}
9631";
9632
9633 let program = Program::parse(initial_source).unwrap().0.unwrap();
9634
9635 let mut frontend = FrontendState::new();
9636
9637 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9638 let mock_ctx = ExecutorContext::new_mock(None).await;
9639 let version = Version(0);
9640
9641 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9642 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9643 let sketch_id = sketch_object.id;
9644 let sketch = expect_sketch(sketch_object);
9645
9646 let point_id = *sketch.segments.get(1).unwrap();
9647
9648 let (src_delta, scene_delta) = frontend
9649 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9650 .await
9651 .unwrap();
9652 insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9653 assert_eq!(scene_delta.new_objects, vec![]);
9654 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9655
9656 ctx.close().await;
9657 mock_ctx.close().await;
9658 }
9659
9660 #[tokio::test(flavor = "multi_thread")]
9661 async fn test_delete_point_with_var() {
9662 let initial_source = "\
9663sketch(on = XY) {
9664 point(at = [var 1, var 2])
9665 point1 = point(at = [var 3, var 4])
9666 point(at = [var 5, var 6])
9667}
9668";
9669
9670 let program = Program::parse(initial_source).unwrap().0.unwrap();
9671
9672 let mut frontend = FrontendState::new();
9673
9674 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9675 let mock_ctx = ExecutorContext::new_mock(None).await;
9676 let version = Version(0);
9677
9678 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9679 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9680 let sketch_id = sketch_object.id;
9681 let sketch = expect_sketch(sketch_object);
9682
9683 let point_id = *sketch.segments.get(1).unwrap();
9684
9685 let (src_delta, scene_delta) = frontend
9686 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9687 .await
9688 .unwrap();
9689 insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9690 assert_eq!(scene_delta.new_objects, vec![]);
9691 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9692
9693 ctx.close().await;
9694 mock_ctx.close().await;
9695 }
9696
9697 #[tokio::test(flavor = "multi_thread")]
9698 async fn test_delete_multiple_points() {
9699 let initial_source = "\
9700sketch(on = XY) {
9701 point(at = [var 1, var 2])
9702 point1 = point(at = [var 3, var 4])
9703 point(at = [var 5, var 6])
9704}
9705";
9706
9707 let program = Program::parse(initial_source).unwrap().0.unwrap();
9708
9709 let mut frontend = FrontendState::new();
9710
9711 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9712 let mock_ctx = ExecutorContext::new_mock(None).await;
9713 let version = Version(0);
9714
9715 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9716 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9717 let sketch_id = sketch_object.id;
9718
9719 let sketch = expect_sketch(sketch_object);
9720
9721 let point1_id = *sketch.segments.first().unwrap();
9722 let point2_id = *sketch.segments.get(1).unwrap();
9723
9724 let (src_delta, scene_delta) = frontend
9725 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9726 .await
9727 .unwrap();
9728 insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9729 assert_eq!(scene_delta.new_objects, vec![]);
9730 assert_eq!(scene_delta.new_graph.objects.len(), 3);
9731
9732 ctx.close().await;
9733 mock_ctx.close().await;
9734 }
9735
9736 #[tokio::test(flavor = "multi_thread")]
9737 async fn test_delete_coincident_constraint() {
9738 let initial_source = "\
9739sketch(on = XY) {
9740 point1 = point(at = [var 1, var 2])
9741 point2 = point(at = [var 3, var 4])
9742 coincident([point1, point2])
9743 point(at = [var 5, var 6])
9744}
9745";
9746
9747 let program = Program::parse(initial_source).unwrap().0.unwrap();
9748
9749 let mut frontend = FrontendState::new();
9750
9751 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9752 let mock_ctx = ExecutorContext::new_mock(None).await;
9753 let version = Version(0);
9754
9755 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9756 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9757 let sketch_id = sketch_object.id;
9758 let sketch = expect_sketch(sketch_object);
9759
9760 let coincident_id = *sketch.constraints.first().unwrap();
9761
9762 let (src_delta, scene_delta) = frontend
9763 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9764 .await
9765 .unwrap();
9766 insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9767 assert_eq!(scene_delta.new_objects, vec![]);
9768 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9769
9770 ctx.close().await;
9771 mock_ctx.close().await;
9772 }
9773
9774 #[tokio::test(flavor = "multi_thread")]
9775 async fn test_delete_line_cascades_to_coincident_constraint() {
9776 let initial_source = "\
9777sketch(on = XY) {
9778 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9779 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9780 coincident([line1.end, line2.start])
9781}
9782";
9783
9784 let program = Program::parse(initial_source).unwrap().0.unwrap();
9785
9786 let mut frontend = FrontendState::new();
9787
9788 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9789 let mock_ctx = ExecutorContext::new_mock(None).await;
9790 let version = Version(0);
9791
9792 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9793 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9794 let sketch_id = sketch_object.id;
9795 let sketch = expect_sketch(sketch_object);
9796 let line_id = *sketch.segments.get(5).unwrap();
9797
9798 let (src_delta, scene_delta) = frontend
9799 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9800 .await
9801 .unwrap();
9802 insta::assert_snapshot!(
9803 "test_delete_line_cascades_to_coincident_constraint",
9804 src_delta.text.as_str()
9805 );
9806 assert_eq!(
9807 scene_delta.new_graph.objects.len(),
9808 5,
9809 "{:#?}",
9810 scene_delta.new_graph.objects
9811 );
9812
9813 ctx.close().await;
9814 mock_ctx.close().await;
9815 }
9816
9817 #[tokio::test(flavor = "multi_thread")]
9818 async fn test_delete_line_cascades_to_distance_constraint() {
9819 let initial_source = "\
9820sketch(on = XY) {
9821 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9822 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9823 distance([line1.end, line2.start]) == 10mm
9824}
9825";
9826
9827 let program = Program::parse(initial_source).unwrap().0.unwrap();
9828
9829 let mut frontend = FrontendState::new();
9830
9831 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9832 let mock_ctx = ExecutorContext::new_mock(None).await;
9833 let version = Version(0);
9834
9835 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9836 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9837 let sketch_id = sketch_object.id;
9838 let sketch = expect_sketch(sketch_object);
9839 let line_id = *sketch.segments.get(5).unwrap();
9840
9841 let (src_delta, scene_delta) = frontend
9842 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9843 .await
9844 .unwrap();
9845 insta::assert_snapshot!(
9846 "test_delete_line_cascades_to_distance_constraint",
9847 src_delta.text.as_str()
9848 );
9849 assert_eq!(
9850 scene_delta.new_graph.objects.len(),
9851 5,
9852 "{:#?}",
9853 scene_delta.new_graph.objects
9854 );
9855
9856 ctx.close().await;
9857 mock_ctx.close().await;
9858 }
9859
9860 #[tokio::test(flavor = "multi_thread")]
9861 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9862 let initial_source = "\
9863sketch(on = XY) {
9864 point1 = point(at = [var 1, var 2])
9865 point2 = point(at = [var 3, var 4])
9866 horizontalDistance([point1, point2]) == 10mm
9867}
9868";
9869
9870 let program = Program::parse(initial_source).unwrap().0.unwrap();
9871
9872 let mut frontend = FrontendState::new();
9873
9874 let mock_ctx = ExecutorContext::new_mock(None).await;
9875 let version = Version(0);
9876
9877 frontend.program = program.clone();
9878 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9879 frontend.update_state_after_exec(outcome, true);
9880 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9881 let sketch_id = sketch_object.id;
9882 let sketch = expect_sketch(sketch_object);
9883 let point2_id = *sketch.segments.get(1).unwrap();
9884
9885 let (src_delta, scene_delta) = frontend
9886 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9887 .await
9888 .unwrap();
9889 insta::assert_snapshot!(
9890 "test_delete_point_cascades_to_horizontal_distance_constraint",
9891 src_delta.text.as_str()
9892 );
9893 assert_eq!(
9894 scene_delta.new_graph.objects.len(),
9895 3,
9896 "{:#?}",
9897 scene_delta.new_graph.objects
9898 );
9899
9900 mock_ctx.close().await;
9901 }
9902
9903 #[tokio::test(flavor = "multi_thread")]
9904 async fn test_delete_line_cascades_to_fixed_constraint() {
9905 let initial_source = "\
9906sketch(on = XY) {
9907 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9908 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9909 fixed([line1.start, [0, 0]])
9910}
9911";
9912
9913 let program = Program::parse(initial_source).unwrap().0.unwrap();
9914
9915 let mut frontend = FrontendState::new();
9916
9917 let mock_ctx = ExecutorContext::new_mock(None).await;
9918 let version = Version(0);
9919
9920 frontend.program = program.clone();
9921 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9922 frontend.update_state_after_exec(outcome, true);
9923 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9924 let sketch_id = sketch_object.id;
9925 let sketch = expect_sketch(sketch_object);
9926 let line1_id = *sketch.segments.get(2).unwrap();
9927
9928 let (src_delta, scene_delta) = frontend
9929 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9930 .await
9931 .unwrap();
9932 insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9933 assert_eq!(
9934 scene_delta.new_graph.objects.len(),
9935 5,
9936 "{:#?}",
9937 scene_delta.new_graph.objects
9938 );
9939
9940 mock_ctx.close().await;
9941 }
9942
9943 #[tokio::test(flavor = "multi_thread")]
9944 async fn test_delete_line_cascades_to_midpoint_constraint() {
9945 let initial_source = "\
9946sketch(on = XY) {
9947 point1 = point(at = [var 1, var 2])
9948 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9949 midpoint(line1, point = point1)
9950}
9951";
9952
9953 let program = Program::parse(initial_source).unwrap().0.unwrap();
9954
9955 let mut frontend = FrontendState::new();
9956
9957 let mock_ctx = ExecutorContext::new_mock(None).await;
9958 let version = Version(0);
9959
9960 frontend.program = program.clone();
9961 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9962 frontend.update_state_after_exec(outcome, true);
9963 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9964 let sketch_id = sketch_object.id;
9965 let sketch = expect_sketch(sketch_object);
9966 let line1_id = *sketch.segments.get(3).unwrap();
9967
9968 let (src_delta, scene_delta) = frontend
9969 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9970 .await
9971 .unwrap();
9972 insta::assert_snapshot!(
9973 "test_delete_line_cascades_to_midpoint_constraint",
9974 src_delta.text.as_str()
9975 );
9976 assert_eq!(
9977 scene_delta.new_graph.objects.len(),
9978 3,
9979 "{:#?}",
9980 scene_delta.new_graph.objects
9981 );
9982
9983 mock_ctx.close().await;
9984 }
9985
9986 #[tokio::test(flavor = "multi_thread")]
9987 async fn test_delete_point_preserves_multiline_coincident_constraint() {
9988 let initial_source = "\
9989sketch(on = XY) {
9990 point1 = point(at = [var 1, var 2])
9991 point2 = point(at = [var 3, var 4])
9992 point3 = point(at = [var 5, var 6])
9993 coincident([point1, point2, point3])
9994}
9995";
9996
9997 let program = Program::parse(initial_source).unwrap().0.unwrap();
9998
9999 let mut frontend = FrontendState::new();
10000
10001 let mock_ctx = ExecutorContext::new_mock(None).await;
10002 let version = Version(0);
10003
10004 frontend.program = program.clone();
10005 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10006 frontend.update_state_after_exec(outcome, true);
10007 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10008 let sketch_id = sketch_object.id;
10009 let sketch = expect_sketch(sketch_object);
10010 let point3_id = *sketch.segments.get(2).unwrap();
10011
10012 let (src_delta, scene_delta) = frontend
10013 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10014 .await
10015 .unwrap();
10016 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10017 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10018 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10019 assert!(
10020 src_delta.text.contains("coincident([point1, point2])"),
10021 "{}",
10022 src_delta.text
10023 );
10024
10025 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10026 let sketch = expect_sketch(sketch_object);
10027 assert_eq!(sketch.segments.len(), 2);
10028 assert_eq!(sketch.constraints.len(), 1);
10029
10030 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10031 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10032 panic!("Expected constraint object");
10033 };
10034 let Constraint::Coincident(coincident) = constraint else {
10035 panic!("Expected coincident constraint");
10036 };
10037 assert_eq!(
10038 coincident.segments,
10039 sketch
10040 .segments
10041 .iter()
10042 .copied()
10043 .map(Into::into)
10044 .collect::<Vec<ConstraintSegment>>()
10045 );
10046
10047 mock_ctx.close().await;
10048 }
10049
10050 #[tokio::test(flavor = "multi_thread")]
10051 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10052 let initial_source = "\
10053sketch(on = XY) {
10054 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10055 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10056 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10057 equalLength([line1, line2, line3])
10058}
10059";
10060
10061 let program = Program::parse(initial_source).unwrap().0.unwrap();
10062
10063 let mut frontend = FrontendState::new();
10064
10065 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10066 let mock_ctx = ExecutorContext::new_mock(None).await;
10067 let version = Version(0);
10068
10069 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10070 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10071 let sketch_id = sketch_object.id;
10072 let sketch = expect_sketch(sketch_object);
10073 let line3_id = *sketch.segments.get(8).unwrap();
10074
10075 let (src_delta, scene_delta) = frontend
10076 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10077 .await
10078 .unwrap();
10079 insta::assert_snapshot!(
10080 "test_delete_line_preserves_multiline_equal_length_constraint",
10081 src_delta.text.as_str()
10082 );
10083
10084 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10085 let sketch = expect_sketch(sketch_object);
10086 assert_eq!(sketch.constraints.len(), 1);
10087
10088 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10089 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10090 panic!("Expected constraint object");
10091 };
10092 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10093 panic!("Expected lines equal length constraint");
10094 };
10095 assert_eq!(lines_equal_length.lines.len(), 2);
10096
10097 ctx.close().await;
10098 mock_ctx.close().await;
10099 }
10100
10101 #[tokio::test(flavor = "multi_thread")]
10102 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10103 let initial_source = "\
10104sketch(on = XY) {
10105 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10106 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10107 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10108 horizontal([line1.end, line2.start, line3.start])
10109}
10110";
10111
10112 let program = Program::parse(initial_source).unwrap().0.unwrap();
10113
10114 let mut frontend = FrontendState::new();
10115
10116 let mock_ctx = ExecutorContext::new_mock(None).await;
10117 let version = Version(0);
10118
10119 frontend.program = program.clone();
10120 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10121 frontend.update_state_after_exec(outcome, true);
10122 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10123 let sketch_id = sketch_object.id;
10124 let sketch = expect_sketch(sketch_object);
10125 let line1_id = *sketch.segments.get(2).unwrap();
10126
10127 let (src_delta, scene_delta) = frontend
10128 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10129 .await
10130 .unwrap();
10131 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10132 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10133 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10134 assert!(
10135 src_delta.text.contains("horizontal([line2.start, line3.start])"),
10136 "{}",
10137 src_delta.text
10138 );
10139
10140 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10141 let sketch = expect_sketch(sketch_object);
10142 assert_eq!(sketch.constraints.len(), 1);
10143
10144 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10145 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10146 panic!("Expected constraint object");
10147 };
10148 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10149 panic!("Expected horizontal points constraint");
10150 };
10151 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10152 assert_eq!(*points, remaining_points);
10153
10154 mock_ctx.close().await;
10155 }
10156
10157 #[tokio::test(flavor = "multi_thread")]
10158 async fn test_delete_line_preserves_multiline_vertical_constraint() {
10159 let initial_source = "\
10160sketch(on = XY) {
10161 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10162 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10163 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10164 vertical([line1.end, line2.start, line3.start])
10165}
10166";
10167
10168 let program = Program::parse(initial_source).unwrap().0.unwrap();
10169
10170 let mut frontend = FrontendState::new();
10171
10172 let mock_ctx = ExecutorContext::new_mock(None).await;
10173 let version = Version(0);
10174
10175 frontend.program = program.clone();
10176 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10177 frontend.update_state_after_exec(outcome, true);
10178 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10179 let sketch_id = sketch_object.id;
10180 let sketch = expect_sketch(sketch_object);
10181 let line1_id = *sketch.segments.get(2).unwrap();
10182
10183 let (src_delta, scene_delta) = frontend
10184 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10185 .await
10186 .unwrap();
10187 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10188 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10189 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10190 assert!(
10191 src_delta.text.contains("vertical([line2.start, line3.start])"),
10192 "{}",
10193 src_delta.text
10194 );
10195
10196 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10197 let sketch = expect_sketch(sketch_object);
10198 assert_eq!(sketch.constraints.len(), 1);
10199
10200 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10201 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10202 panic!("Expected constraint object");
10203 };
10204 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10205 panic!("Expected vertical points constraint");
10206 };
10207 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10208 assert_eq!(*points, remaining_points);
10209
10210 mock_ctx.close().await;
10211 }
10212
10213 #[tokio::test(flavor = "multi_thread")]
10214 async fn test_delete_line_preserves_multiline_coincident_constraint() {
10215 let initial_source = "\
10216sketch(on = XY) {
10217 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10218 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10219 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10220 coincident([line1.end, line2.start, line3.start])
10221}
10222";
10223
10224 let program = Program::parse(initial_source).unwrap().0.unwrap();
10225
10226 let mut frontend = FrontendState::new();
10227
10228 let mock_ctx = ExecutorContext::new_mock(None).await;
10229 let version = Version(0);
10230
10231 frontend.program = program.clone();
10232 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10233 frontend.update_state_after_exec(outcome, true);
10234 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10235 let sketch_id = sketch_object.id;
10236 let sketch = expect_sketch(sketch_object);
10237 let line1_id = *sketch.segments.get(2).unwrap();
10238
10239 let (src_delta, scene_delta) = frontend
10240 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10241 .await
10242 .unwrap();
10243 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10244 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10245 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10246 assert!(
10247 src_delta.text.contains("coincident([line2.start, line3.start])"),
10248 "{}",
10249 src_delta.text
10250 );
10251
10252 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10253 let sketch = expect_sketch(sketch_object);
10254 assert_eq!(sketch.constraints.len(), 1);
10255
10256 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10257 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10258 panic!("Expected constraint object");
10259 };
10260 let Constraint::Coincident(coincident) = constraint else {
10261 panic!("Expected coincident constraint");
10262 };
10263 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10264 assert_eq!(coincident.segments, remaining_segments);
10265
10266 mock_ctx.close().await;
10267 }
10268
10269 #[tokio::test(flavor = "multi_thread")]
10270 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10271 let initial_source = "\
10272sketch(on = XY) {
10273 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10274 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10275 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10276 equalLength([line1, line2, line3])
10277}
10278";
10279
10280 let program = Program::parse(initial_source).unwrap().0.unwrap();
10281
10282 let mut frontend = FrontendState::new();
10283
10284 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10285 let mock_ctx = ExecutorContext::new_mock(None).await;
10286 let version = Version(0);
10287
10288 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10289 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10290 let sketch_id = sketch_object.id;
10291 let sketch = expect_sketch(sketch_object);
10292 let line2_id = *sketch.segments.get(5).unwrap();
10293 let line3_id = *sketch.segments.get(8).unwrap();
10294
10295 let (src_delta, scene_delta) = frontend
10296 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10297 .await
10298 .unwrap();
10299 insta::assert_snapshot!(
10300 "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10301 src_delta.text.as_str()
10302 );
10303
10304 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10305 let sketch = expect_sketch(sketch_object);
10306 assert!(sketch.constraints.is_empty());
10307
10308 ctx.close().await;
10309 mock_ctx.close().await;
10310 }
10311
10312 #[tokio::test(flavor = "multi_thread")]
10313 async fn test_delete_line_preserves_multiline_parallel_constraint() {
10314 let initial_source = "\
10315sketch(on = XY) {
10316 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10317 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10318 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10319 parallel([line1, line2, line3])
10320}
10321";
10322
10323 let program = Program::parse(initial_source).unwrap().0.unwrap();
10324
10325 let mut frontend = FrontendState::new();
10326
10327 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10328 let mock_ctx = ExecutorContext::new_mock(None).await;
10329 let version = Version(0);
10330
10331 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10332 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10333 let sketch_id = sketch_object.id;
10334 let sketch = expect_sketch(sketch_object);
10335 let line3_id = *sketch.segments.get(8).unwrap();
10336
10337 let (src_delta, scene_delta) = frontend
10338 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10339 .await
10340 .unwrap();
10341 insta::assert_snapshot!(
10342 "test_delete_line_preserves_multiline_parallel_constraint",
10343 src_delta.text.as_str()
10344 );
10345
10346 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10347 let sketch = expect_sketch(sketch_object);
10348 assert_eq!(sketch.constraints.len(), 1);
10349
10350 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10351 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10352 panic!("Expected constraint object");
10353 };
10354 let Constraint::Parallel(parallel) = constraint else {
10355 panic!("Expected parallel constraint");
10356 };
10357 assert_eq!(parallel.lines.len(), 2);
10358
10359 ctx.close().await;
10360 mock_ctx.close().await;
10361 }
10362
10363 #[tokio::test(flavor = "multi_thread")]
10364 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10365 let initial_source = "\
10366sketch(on = XY) {
10367 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10368 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10369 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10370 parallel([line1, line2, line3])
10371}
10372";
10373
10374 let program = Program::parse(initial_source).unwrap().0.unwrap();
10375
10376 let mut frontend = FrontendState::new();
10377
10378 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10379 let mock_ctx = ExecutorContext::new_mock(None).await;
10380 let version = Version(0);
10381
10382 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10383 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10384 let sketch_id = sketch_object.id;
10385 let sketch = expect_sketch(sketch_object);
10386 let line2_id = *sketch.segments.get(5).unwrap();
10387 let line3_id = *sketch.segments.get(8).unwrap();
10388
10389 let (src_delta, scene_delta) = frontend
10390 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10391 .await
10392 .unwrap();
10393 insta::assert_snapshot!(
10394 "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10395 src_delta.text.as_str()
10396 );
10397
10398 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10399 let sketch = expect_sketch(sketch_object);
10400 assert!(sketch.constraints.is_empty());
10401
10402 ctx.close().await;
10403 mock_ctx.close().await;
10404 }
10405
10406 #[tokio::test(flavor = "multi_thread")]
10407 async fn test_delete_line_line_coincident_constraint() {
10408 let initial_source = "\
10409sketch(on = XY) {
10410 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10411 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10412 coincident([line1, line2])
10413}
10414";
10415
10416 let program = Program::parse(initial_source).unwrap().0.unwrap();
10417
10418 let mut frontend = FrontendState::new();
10419
10420 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10421 let mock_ctx = ExecutorContext::new_mock(None).await;
10422 let version = Version(0);
10423
10424 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10425 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10426 let sketch_id = sketch_object.id;
10427 let sketch = expect_sketch(sketch_object);
10428
10429 let coincident_id = *sketch.constraints.first().unwrap();
10430
10431 let (src_delta, scene_delta) = frontend
10432 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10433 .await
10434 .unwrap();
10435 insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10436 assert_eq!(scene_delta.new_objects, vec![]);
10437 assert_eq!(scene_delta.new_graph.objects.len(), 8);
10438
10439 ctx.close().await;
10440 mock_ctx.close().await;
10441 }
10442
10443 #[tokio::test(flavor = "multi_thread")]
10444 async fn test_two_points_coincident() {
10445 let initial_source = "\
10446sketch(on = XY) {
10447 point1 = point(at = [var 1, var 2])
10448 point(at = [3, 4])
10449}
10450";
10451
10452 let program = Program::parse(initial_source).unwrap().0.unwrap();
10453
10454 let mut frontend = FrontendState::new();
10455
10456 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10457 let mock_ctx = ExecutorContext::new_mock(None).await;
10458 let version = Version(0);
10459
10460 frontend.hack_set_program(&ctx, program).await.unwrap();
10461 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10462 let sketch_id = sketch_object.id;
10463 let sketch = expect_sketch(sketch_object);
10464 let point0_id = *sketch.segments.first().unwrap();
10465 let point1_id = *sketch.segments.get(1).unwrap();
10466
10467 let constraint = Constraint::Coincident(Coincident {
10468 segments: vec![point0_id.into(), point1_id.into()],
10469 });
10470 let (src_delta, scene_delta) = frontend
10471 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10472 .await
10473 .unwrap();
10474 insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10475 assert_eq!(
10476 scene_delta.new_graph.objects.len(),
10477 5,
10478 "{:#?}",
10479 scene_delta.new_graph.objects
10480 );
10481
10482 ctx.close().await;
10483 mock_ctx.close().await;
10484 }
10485
10486 #[tokio::test(flavor = "multi_thread")]
10487 async fn test_three_points_coincident() {
10488 let initial_source = "\
10489sketch(on = XY) {
10490 point1 = point(at = [var 1, var 2])
10491 point(at = [var 3, var 4])
10492 point(at = [var 5, var 6])
10493}
10494";
10495
10496 let program = Program::parse(initial_source).unwrap().0.unwrap();
10497
10498 let mut frontend = FrontendState::new();
10499
10500 let mock_ctx = ExecutorContext::new_mock(None).await;
10501 let version = Version(0);
10502
10503 frontend.program = program.clone();
10504 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10505 frontend.update_state_after_exec(outcome, true);
10506 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10507 let sketch_id = sketch_object.id;
10508 let sketch = expect_sketch(sketch_object);
10509 let segments = sketch
10510 .segments
10511 .iter()
10512 .take(3)
10513 .copied()
10514 .map(Into::into)
10515 .collect::<Vec<ConstraintSegment>>();
10516
10517 let constraint = Constraint::Coincident(Coincident {
10518 segments: segments.clone(),
10519 });
10520 let (src_delta, scene_delta) = frontend
10521 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10522 .await
10523 .unwrap();
10524 insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10525
10526 let constraint_object = scene_delta
10527 .new_graph
10528 .objects
10529 .iter()
10530 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10531 .unwrap();
10532
10533 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10534 panic!("expected a constraint object");
10535 };
10536
10537 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10538
10539 mock_ctx.close().await;
10540 }
10541
10542 #[tokio::test(flavor = "multi_thread")]
10543 async fn test_source_with_three_point_coincident_tracks_all_segments() {
10544 let initial_source = "\
10545sketch(on = XY) {
10546 point1 = point(at = [var 1, var 2])
10547 point2 = point(at = [var 3, var 4])
10548 point3 = point(at = [var 5, var 6])
10549 coincident([point1, point2, point3])
10550}
10551";
10552
10553 let program = Program::parse(initial_source).unwrap().0.unwrap();
10554
10555 let mut frontend = FrontendState::new();
10556
10557 let ctx = ExecutorContext::new_mock(None).await;
10558 frontend.program = program.clone();
10559 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10560 frontend.update_state_after_exec(outcome, true);
10561
10562 let constraint_object = frontend
10563 .scene_graph
10564 .objects
10565 .iter()
10566 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10567 .unwrap();
10568 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10569 panic!("expected a constraint object");
10570 };
10571
10572 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10573 let sketch = expect_sketch(sketch_object);
10574 let expected_segments = sketch
10575 .segments
10576 .iter()
10577 .take(3)
10578 .copied()
10579 .map(Into::into)
10580 .collect::<Vec<ConstraintSegment>>();
10581
10582 assert_eq!(
10583 constraint,
10584 &Constraint::Coincident(Coincident {
10585 segments: expected_segments,
10586 })
10587 );
10588
10589 ctx.close().await;
10590 }
10591
10592 #[tokio::test(flavor = "multi_thread")]
10593 async fn test_point_origin_coincident_preserves_order() {
10594 let initial_source = "\
10595sketch(on = XY) {
10596 point(at = [var 1, var 2])
10597}
10598";
10599
10600 for (origin_first, snapshot_name) in [
10601 (true, "test_point_origin_coincident_preserves_order_origin_first"),
10602 (false, "test_point_origin_coincident_preserves_order_point_first"),
10603 ] {
10604 let program = Program::parse(initial_source).unwrap().0.unwrap();
10605
10606 let mut frontend = FrontendState::new();
10607
10608 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10609 let mock_ctx = ExecutorContext::new_mock(None).await;
10610 let version = Version(0);
10611
10612 frontend.hack_set_program(&ctx, program).await.unwrap();
10613 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10614 let sketch_id = sketch_object.id;
10615 let sketch = expect_sketch(sketch_object);
10616 let point_id = *sketch.segments.first().unwrap();
10617
10618 let segments = if origin_first {
10619 vec![ConstraintSegment::ORIGIN, point_id.into()]
10620 } else {
10621 vec![point_id.into(), ConstraintSegment::ORIGIN]
10622 };
10623 let constraint = Constraint::Coincident(Coincident {
10624 segments: segments.clone(),
10625 });
10626 let (src_delta, scene_delta) = frontend
10627 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10628 .await
10629 .unwrap();
10630 insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10631
10632 let constraint_object = scene_delta
10633 .new_graph
10634 .objects
10635 .iter()
10636 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10637 .unwrap();
10638
10639 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10640 panic!("expected a constraint object");
10641 };
10642
10643 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10644
10645 ctx.close().await;
10646 mock_ctx.close().await;
10647 }
10648 }
10649
10650 #[tokio::test(flavor = "multi_thread")]
10651 async fn test_coincident_of_line_end_points() {
10652 let initial_source = "\
10653sketch(on = XY) {
10654 line(start = [var 1, var 2], end = [var 3, var 4])
10655 line(start = [var 5, var 6], end = [var 7, var 8])
10656}
10657";
10658
10659 let program = Program::parse(initial_source).unwrap().0.unwrap();
10660
10661 let mut frontend = FrontendState::new();
10662
10663 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10664 let mock_ctx = ExecutorContext::new_mock(None).await;
10665 let version = Version(0);
10666
10667 frontend.hack_set_program(&ctx, program).await.unwrap();
10668 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10669 let sketch_id = sketch_object.id;
10670 let sketch = expect_sketch(sketch_object);
10671 let point0_id = *sketch.segments.get(1).unwrap();
10672 let point1_id = *sketch.segments.get(3).unwrap();
10673
10674 let constraint = Constraint::Coincident(Coincident {
10675 segments: vec![point0_id.into(), point1_id.into()],
10676 });
10677 let (src_delta, scene_delta) = frontend
10678 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10679 .await
10680 .unwrap();
10681 insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10682 assert_eq!(
10683 scene_delta.new_graph.objects.len(),
10684 9,
10685 "{:#?}",
10686 scene_delta.new_graph.objects
10687 );
10688
10689 ctx.close().await;
10690 mock_ctx.close().await;
10691 }
10692
10693 #[tokio::test(flavor = "multi_thread")]
10694 async fn test_coincident_of_line_point_and_circle_segment() {
10695 let initial_source = "\
10696sketch(on = XY) {
10697 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10698 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10699}
10700";
10701 let program = Program::parse(initial_source).unwrap().0.unwrap();
10702 let mut frontend = FrontendState::new();
10703
10704 let mock_ctx = ExecutorContext::new_mock(None).await;
10705 let version = Version(0);
10706
10707 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10708 frontend.program = program;
10709 frontend.update_state_after_exec(outcome, true);
10710 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10711 let sketch_id = sketch_object.id;
10712 let sketch = expect_sketch(sketch_object);
10713
10714 let circle_id = sketch
10715 .segments
10716 .iter()
10717 .copied()
10718 .find(|seg_id| {
10719 matches!(
10720 &frontend.scene_graph.objects[seg_id.0].kind,
10721 ObjectKind::Segment {
10722 segment: Segment::Circle(_)
10723 }
10724 )
10725 })
10726 .expect("Expected a circle segment in sketch");
10727 let line_id = frontend
10728 .scene_graph
10729 .objects
10730 .iter()
10731 .find_map(|obj| match &obj.kind {
10732 ObjectKind::Segment {
10733 segment: Segment::Line(line),
10734 } if line.owner.is_none() => Some(obj.id),
10735 _ => None,
10736 })
10737 .expect("Expected a standalone line segment in scene graph");
10738
10739 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10740 ObjectKind::Segment {
10741 segment: Segment::Line(line),
10742 } => line.start,
10743 _ => panic!("Expected line segment object"),
10744 };
10745
10746 let constraint = Constraint::Coincident(Coincident {
10747 segments: vec![line_start_point_id.into(), circle_id.into()],
10748 });
10749 let (src_delta, _scene_delta) = frontend
10750 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10751 .await
10752 .unwrap();
10753 insta::assert_snapshot!(
10754 "test_coincident_of_line_point_and_circle_segment",
10755 src_delta.text.as_str()
10756 );
10757
10758 mock_ctx.close().await;
10759 }
10760
10761 #[tokio::test(flavor = "multi_thread")]
10762 async fn test_invalid_coincident_arc_and_line_preserves_state() {
10763 let program = Program::empty();
10771
10772 let mut frontend = FrontendState::new();
10773 frontend.program = program;
10774
10775 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10776 let mock_ctx = ExecutorContext::new_mock(None).await;
10777 let version = Version(0);
10778
10779 let sketch_args = SketchCtor {
10780 on: Plane::Default(PlaneName::Xy),
10781 };
10782 let (_src_delta, _scene_delta, sketch_id) = frontend
10783 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10784 .await
10785 .unwrap();
10786
10787 let arc_ctor = ArcCtor {
10789 start: Point2d {
10790 x: Expr::Var(Number {
10791 value: 0.0,
10792 units: NumericSuffix::Mm,
10793 }),
10794 y: Expr::Var(Number {
10795 value: 0.0,
10796 units: NumericSuffix::Mm,
10797 }),
10798 },
10799 end: Point2d {
10800 x: Expr::Var(Number {
10801 value: 10.0,
10802 units: NumericSuffix::Mm,
10803 }),
10804 y: Expr::Var(Number {
10805 value: 10.0,
10806 units: NumericSuffix::Mm,
10807 }),
10808 },
10809 center: Point2d {
10810 x: Expr::Var(Number {
10811 value: 10.0,
10812 units: NumericSuffix::Mm,
10813 }),
10814 y: Expr::Var(Number {
10815 value: 0.0,
10816 units: NumericSuffix::Mm,
10817 }),
10818 },
10819 direction: None,
10820 construction: None,
10821 };
10822 let (_src_delta, scene_delta) = frontend
10823 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10824 .await
10825 .unwrap();
10826 let arc_id = *scene_delta.new_objects.last().unwrap();
10828
10829 let line_ctor = LineCtor {
10831 start: Point2d {
10832 x: Expr::Var(Number {
10833 value: 20.0,
10834 units: NumericSuffix::Mm,
10835 }),
10836 y: Expr::Var(Number {
10837 value: 0.0,
10838 units: NumericSuffix::Mm,
10839 }),
10840 },
10841 end: Point2d {
10842 x: Expr::Var(Number {
10843 value: 30.0,
10844 units: NumericSuffix::Mm,
10845 }),
10846 y: Expr::Var(Number {
10847 value: 10.0,
10848 units: NumericSuffix::Mm,
10849 }),
10850 },
10851 construction: None,
10852 };
10853 let (_src_delta, scene_delta) = frontend
10854 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10855 .await
10856 .unwrap();
10857 let line_id = *scene_delta.new_objects.last().unwrap();
10859
10860 let constraint = Constraint::Coincident(Coincident {
10863 segments: vec![arc_id.into(), line_id.into()],
10864 });
10865 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10866
10867 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10869
10870 let sketch_object_after =
10873 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10874 let sketch_after = expect_sketch(sketch_object_after);
10875
10876 assert!(
10878 sketch_after.segments.contains(&arc_id),
10879 "Arc segment should still exist after failed constraint"
10880 );
10881 assert!(
10882 sketch_after.segments.contains(&line_id),
10883 "Line segment should still exist after failed constraint"
10884 );
10885
10886 let arc_obj = frontend
10888 .scene_graph
10889 .objects
10890 .get(arc_id.0)
10891 .expect("Arc object should still be accessible");
10892 let line_obj = frontend
10893 .scene_graph
10894 .objects
10895 .get(line_id.0)
10896 .expect("Line object should still be accessible");
10897
10898 match &arc_obj.kind {
10901 ObjectKind::Segment {
10902 segment: Segment::Arc(_),
10903 } => {}
10904 _ => panic!("Arc object should still be an arc segment"),
10905 }
10906 match &line_obj.kind {
10907 ObjectKind::Segment {
10908 segment: Segment::Line(_),
10909 } => {}
10910 _ => panic!("Line object should still be a line segment"),
10911 }
10912
10913 ctx.close().await;
10914 mock_ctx.close().await;
10915 }
10916
10917 #[tokio::test(flavor = "multi_thread")]
10918 async fn test_distance_two_points() {
10919 let initial_source = "\
10920sketch(on = XY) {
10921 point(at = [var 1, var 2])
10922 point(at = [var 3, var 4])
10923}
10924";
10925
10926 let program = Program::parse(initial_source).unwrap().0.unwrap();
10927
10928 let mut frontend = FrontendState::new();
10929
10930 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10931 let mock_ctx = ExecutorContext::new_mock(None).await;
10932 let version = Version(0);
10933
10934 frontend.hack_set_program(&ctx, program).await.unwrap();
10935 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10936 let sketch_id = sketch_object.id;
10937 let sketch = expect_sketch(sketch_object);
10938 let point0_id = *sketch.segments.first().unwrap();
10939 let point1_id = *sketch.segments.get(1).unwrap();
10940
10941 let constraint = Constraint::Distance(Distance {
10942 segments: vec![point0_id.into(), point1_id.into()],
10943 distance: Number {
10944 value: 2.0,
10945 units: NumericSuffix::Mm,
10946 },
10947 label_position: None,
10948 source: Default::default(),
10949 });
10950 let (src_delta, scene_delta) = frontend
10951 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10952 .await
10953 .unwrap();
10954 insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
10955 assert_eq!(
10956 scene_delta.new_graph.objects.len(),
10957 5,
10958 "{:#?}",
10959 scene_delta.new_graph.objects
10960 );
10961
10962 ctx.close().await;
10963 mock_ctx.close().await;
10964 }
10965
10966 #[tokio::test(flavor = "multi_thread")]
10967 async fn test_distance_two_points_with_label() {
10968 let initial_source = "\
10969sketch(on = XY) {
10970 point(at = [var 1, var 2])
10971 point(at = [var 3, var 4])
10972}
10973";
10974
10975 let program = Program::parse(initial_source).unwrap().0.unwrap();
10976
10977 let mut frontend = FrontendState::new();
10978
10979 let mock_ctx = ExecutorContext::new_mock(None).await;
10980 let version = Version(0);
10981
10982 frontend.program = program.clone();
10983 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10984 frontend.update_state_after_exec(outcome, true);
10985 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10986 let sketch_id = sketch_object.id;
10987 let sketch = expect_sketch(sketch_object);
10988 let point0_id = *sketch.segments.first().unwrap();
10989 let point1_id = *sketch.segments.get(1).unwrap();
10990
10991 let label_position = Point2d {
10992 x: Number {
10993 value: 10.0,
10994 units: NumericSuffix::Mm,
10995 },
10996 y: Number {
10997 value: 11.0,
10998 units: NumericSuffix::Mm,
10999 },
11000 };
11001 let constraint = Constraint::Distance(Distance {
11002 segments: vec![point0_id.into(), point1_id.into()],
11003 distance: Number {
11004 value: 2.0,
11005 units: NumericSuffix::Mm,
11006 },
11007 label_position: Some(label_position.clone()),
11008 source: Default::default(),
11009 });
11010 let (src_delta, scene_delta) = frontend
11011 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11012 .await
11013 .unwrap();
11014 insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11015
11016 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11017 let sketch = expect_sketch(sketch_object);
11018 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11019 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11020 panic!("Expected constraint object");
11021 };
11022 let Constraint::Distance(distance) = constraint else {
11023 panic!("Expected distance constraint");
11024 };
11025 assert_eq!(distance.label_position, Some(label_position));
11026
11027 mock_ctx.close().await;
11028 }
11029
11030 #[tokio::test(flavor = "multi_thread")]
11031 async fn test_edit_distance_constraint_label_position() {
11032 let initial_source = "\
11033sketch(on = XY) {
11034 point(at = [var 1, var 2])
11035 point(at = [var 3, var 2])
11036}
11037";
11038
11039 let program = Program::parse(initial_source).unwrap().0.unwrap();
11040
11041 let mut frontend = FrontendState::new();
11042
11043 let mock_ctx = ExecutorContext::new_mock(None).await;
11044 let version = Version(0);
11045
11046 frontend.program = program.clone();
11047 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11048 frontend.update_state_after_exec(outcome, true);
11049 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11050 let sketch_id = sketch_object.id;
11051 let sketch = expect_sketch(sketch_object);
11052 let point0_id = *sketch.segments.first().unwrap();
11053 let point1_id = *sketch.segments.get(1).unwrap();
11054
11055 let constraint = Constraint::Distance(Distance {
11056 segments: vec![point0_id.into(), point1_id.into()],
11057 distance: Number {
11058 value: 2.0,
11059 units: NumericSuffix::Mm,
11060 },
11061 label_position: None,
11062 source: Default::default(),
11063 });
11064 let (_, scene_delta) = frontend
11065 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11066 .await
11067 .unwrap();
11068 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11069 let sketch = expect_sketch(sketch_object);
11070 let constraint_id = sketch.constraints[0];
11071 let label_position = Point2d {
11072 x: Number {
11073 value: 10.0,
11074 units: NumericSuffix::Mm,
11075 },
11076 y: Number {
11077 value: 11.0,
11078 units: NumericSuffix::Mm,
11079 },
11080 };
11081
11082 let (src_delta, scene_delta) = frontend
11083 .edit_distance_constraint_label_position(
11084 &mock_ctx,
11085 version,
11086 sketch_id,
11087 constraint_id,
11088 label_position.clone(),
11089 vec![],
11090 )
11091 .await
11092 .unwrap();
11093 insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11094
11095 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11096 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11097 panic!("Expected constraint object");
11098 };
11099 let Constraint::Distance(distance) = constraint else {
11100 panic!("Expected distance constraint");
11101 };
11102 assert_eq!(distance.label_position, Some(label_position));
11103
11104 mock_ctx.close().await;
11105 }
11106
11107 #[tokio::test(flavor = "multi_thread")]
11108 async fn test_edit_distance_constraint_type_and_value() {
11109 let initial_source = "\
11110sketch(on = XY) {
11111 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11112 distance([line1.start, line1.end]) == 5mm
11113}
11114";
11115
11116 let program = Program::parse(initial_source).unwrap().0.unwrap();
11117 let mut frontend = FrontendState::new();
11118 let mock_ctx = ExecutorContext::new_mock(None).await;
11119 let version = Version(0);
11120
11121 frontend.program = program.clone();
11122 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11123 frontend.update_state_after_exec(outcome, true);
11124 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11125 let sketch_id = sketch_object.id;
11126 let sketch = expect_sketch(sketch_object);
11127 let constraint_id = sketch.constraints[0];
11128 let point0_id = sketch.segments[0];
11129 let point1_id = sketch.segments[1];
11130 let label_position = Point2d {
11131 x: Number {
11132 value: 2.0,
11133 units: NumericSuffix::Mm,
11134 },
11135 y: Number {
11136 value: 5.0,
11137 units: NumericSuffix::Mm,
11138 },
11139 };
11140
11141 let (source_delta, scene_delta) = frontend
11142 .edit_distance_constraint_with_options(
11143 &mock_ctx,
11144 version,
11145 sketch_id,
11146 constraint_id,
11147 Constraint::HorizontalDistance(Distance {
11148 segments: vec![point0_id.into(), point1_id.into()],
11149 distance: Number {
11150 value: 4.0,
11151 units: NumericSuffix::Mm,
11152 },
11153 label_position: Some(label_position.clone()),
11154 source: Default::default(),
11155 }),
11156 EditConstraintOptions {
11157 commit_solved_initial_guesses: false,
11158 },
11159 )
11160 .await
11161 .unwrap();
11162 assert_eq!(
11163 source_delta.text,
11164 "\
11165sketch(on = XY) {
11166 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11167 horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11168}
11169"
11170 );
11171
11172 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11173 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11174 panic!("Expected constraint object");
11175 };
11176 let Constraint::HorizontalDistance(distance) = constraint else {
11177 panic!("Expected horizontal distance constraint");
11178 };
11179 assert_eq!(distance.distance.value, 4.0);
11180 assert_eq!(distance.label_position, Some(label_position));
11181
11182 mock_ctx.close().await;
11183 }
11184
11185 #[tokio::test(flavor = "multi_thread")]
11186 async fn test_edit_angle_constraint_label_position() {
11187 let initial_source = "\
11188sketch(on = XY) {
11189 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11190 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11191 angle([line1, line2]) == 60deg
11192}
11193";
11194
11195 let program = Program::parse(initial_source).unwrap().0.unwrap();
11196 let mut frontend = FrontendState::new();
11197 let mock_ctx = ExecutorContext::new_mock(None).await;
11198 let version = Version(0);
11199
11200 frontend.program = program.clone();
11201 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11202 frontend.update_state_after_exec(outcome, true);
11203 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11204 let sketch_id = sketch_object.id;
11205 let sketch = expect_sketch(sketch_object);
11206 let constraint_id = sketch.constraints[0];
11207 let label_position = Point2d {
11208 x: Number {
11209 value: 10.0,
11210 units: NumericSuffix::Mm,
11211 },
11212 y: Number {
11213 value: 11.0,
11214 units: NumericSuffix::Mm,
11215 },
11216 };
11217
11218 let (src_delta, scene_delta) = frontend
11219 .edit_distance_constraint_label_position(
11220 &mock_ctx,
11221 version,
11222 sketch_id,
11223 constraint_id,
11224 label_position.clone(),
11225 vec![],
11226 )
11227 .await
11228 .unwrap();
11229 assert_eq!(
11230 src_delta.text.as_str(),
11231 "\
11232sketch(on = XY) {
11233 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11234 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11235 angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11236}
11237"
11238 );
11239
11240 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11241 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11242 panic!("Expected constraint object");
11243 };
11244 let Constraint::Angle(angle) = constraint else {
11245 panic!("Expected angle constraint");
11246 };
11247 assert_eq!(angle.label_position, Some(label_position));
11248
11249 mock_ctx.close().await;
11250 }
11251
11252 #[tokio::test(flavor = "multi_thread")]
11253 async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11254 let initial_source = "\
11255sketch(on = XY) {
11256 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11257 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11258 60deg == angleDimension(lines = [line1, line2], sector = 1)
11259}
11260";
11261
11262 let program = Program::parse(initial_source).unwrap().0.unwrap();
11263 let mut frontend = FrontendState::new();
11264 let mock_ctx = ExecutorContext::new_mock(None).await;
11265 let version = Version(0);
11266
11267 frontend.program = program.clone();
11268 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11269 frontend.update_state_after_exec(outcome, true);
11270 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11271 let sketch_id = sketch_object.id;
11272 let sketch = expect_sketch(sketch_object);
11273 let constraint_id = sketch.constraints[0];
11274 let label_position = Point2d {
11275 x: Number {
11276 value: 10.0,
11277 units: NumericSuffix::Mm,
11278 },
11279 y: Number {
11280 value: 11.0,
11281 units: NumericSuffix::Mm,
11282 },
11283 };
11284
11285 let (src_delta, scene_delta) = frontend
11286 .edit_distance_constraint_label_position(
11287 &mock_ctx,
11288 version,
11289 sketch_id,
11290 constraint_id,
11291 label_position.clone(),
11292 vec![],
11293 )
11294 .await
11295 .unwrap();
11296 assert_eq!(
11297 src_delta.text.as_str(),
11298 "\
11299sketch(on = XY) {
11300 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11301 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11302 60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11303}
11304"
11305 );
11306
11307 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11308 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11309 panic!("Expected constraint object");
11310 };
11311 let Constraint::Angle(angle) = constraint else {
11312 panic!("Expected angle constraint");
11313 };
11314 assert_eq!(angle.label_position, Some(label_position));
11315
11316 mock_ctx.close().await;
11317 }
11318
11319 #[tokio::test(flavor = "multi_thread")]
11320 async fn test_edit_angle_constraint() {
11321 let initial_source = "\
11322sketch(on = XY) {
11323 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11324 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11325 angle([line1, line2]) == 60deg
11326}
11327";
11328
11329 let program = Program::parse(initial_source).unwrap().0.unwrap();
11330 let mut frontend = FrontendState::new();
11331 let mock_ctx = ExecutorContext::new_mock(None).await;
11332 let version = Version(0);
11333
11334 frontend.program = program.clone();
11335 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11336 frontend.update_state_after_exec(outcome, true);
11337 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11338 let sketch_id = sketch_object.id;
11339 let sketch = expect_sketch(sketch_object);
11340 let constraint_id = sketch.constraints[0];
11341 let line1_id = *sketch.segments.get(2).unwrap();
11342 let line2_id = *sketch.segments.get(5).unwrap();
11343 let label_position = Point2d {
11344 x: Number {
11345 value: 10.0,
11346 units: NumericSuffix::Mm,
11347 },
11348 y: Number {
11349 value: 11.0,
11350 units: NumericSuffix::Mm,
11351 },
11352 };
11353
11354 let (src_delta, scene_delta) = frontend
11355 .edit_angle_constraint_with_options(
11356 &mock_ctx,
11357 version,
11358 sketch_id,
11359 constraint_id,
11360 Angle {
11361 lines: vec![line2_id, line1_id],
11362 angle: Number {
11363 value: 60.0,
11364 units: NumericSuffix::Deg,
11365 },
11366 sector: Some(3),
11367 inverse: Some(false),
11368 label_position: Some(label_position.clone()),
11369 source: Default::default(),
11370 },
11371 EditConstraintOptions {
11372 commit_solved_initial_guesses: false,
11373 },
11374 )
11375 .await
11376 .unwrap();
11377 assert_eq!(
11378 src_delta.text.as_str(),
11379 "\
11380sketch(on = XY) {
11381 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11382 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11383 angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11384}
11385"
11386 );
11387
11388 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11389 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11390 panic!("Expected constraint object");
11391 };
11392 let Constraint::Angle(angle) = constraint else {
11393 panic!("Expected angle constraint");
11394 };
11395 assert_eq!(angle.lines, vec![line2_id, line1_id]);
11396 assert_eq!(angle.sector, Some(3));
11397 assert_eq!(angle.inverse, Some(false));
11398 assert_eq!(angle.label_position, Some(label_position));
11399
11400 mock_ctx.close().await;
11401 }
11402
11403 #[tokio::test(flavor = "multi_thread")]
11404 async fn test_edit_angle_constraint_with_call_on_right() {
11405 let initial_source = "\
11406sketch(on = XY) {
11407 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11408 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11409 60deg == angle([line1, line2])
11410}
11411";
11412
11413 let program = Program::parse(initial_source).unwrap().0.unwrap();
11414 let mut frontend = FrontendState::new();
11415 let mock_ctx = ExecutorContext::new_mock(None).await;
11416 let version = Version(0);
11417
11418 frontend.program = program.clone();
11419 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11420 frontend.update_state_after_exec(outcome, true);
11421 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11422 let sketch_id = sketch_object.id;
11423 let sketch = expect_sketch(sketch_object);
11424 let constraint_id = sketch.constraints[0];
11425 let line1_id = *sketch.segments.get(2).unwrap();
11426 let line2_id = *sketch.segments.get(5).unwrap();
11427
11428 let (src_delta, _) = frontend
11429 .edit_angle_constraint_with_options(
11430 &mock_ctx,
11431 version,
11432 sketch_id,
11433 constraint_id,
11434 Angle {
11435 lines: vec![line2_id, line1_id],
11436 angle: Number {
11437 value: 60.0,
11438 units: NumericSuffix::Deg,
11439 },
11440 sector: Some(3),
11441 inverse: Some(false),
11442 label_position: None,
11443 source: Default::default(),
11444 },
11445 EditConstraintOptions {
11446 commit_solved_initial_guesses: false,
11447 },
11448 )
11449 .await
11450 .unwrap();
11451 assert_eq!(
11452 src_delta.text.as_str(),
11453 "\
11454sketch(on = XY) {
11455 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11456 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11457 60deg == angleDimension(lines = [line2, line1], sector = 3)
11458}
11459"
11460 );
11461
11462 mock_ctx.close().await;
11463 }
11464
11465 #[tokio::test(flavor = "multi_thread")]
11466 async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11467 let initial_source = "\
11468@settings(kclVersion = 2.0)
11469
11470sketch001 = sketch(on = XZ) {
11471 line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11472 line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11473 distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11474 vertical([line1.start, ORIGIN])
11475}
11476";
11477
11478 let program = Program::parse(initial_source).unwrap().0.unwrap();
11479 let mut frontend = FrontendState::new();
11480 let mock_ctx = ExecutorContext::new_mock(None).await;
11481 let version = Version(0);
11482
11483 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11484 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11485 let sketch_id = sketch_object.id;
11486 let sketch = expect_sketch(sketch_object);
11487 let constraint_id = sketch
11488 .constraints
11489 .iter()
11490 .copied()
11491 .find(|constraint_id| {
11492 matches!(
11493 frontend.scene_graph.objects[constraint_id.0].kind,
11494 ObjectKind::Constraint {
11495 constraint: Constraint::Distance(_)
11496 }
11497 )
11498 })
11499 .unwrap();
11500 let line1_id = sketch
11501 .segments
11502 .iter()
11503 .copied()
11504 .find(|segment_id| {
11505 matches!(
11506 frontend.scene_graph.objects[segment_id.0].kind,
11507 ObjectKind::Segment {
11508 segment: Segment::Line(_)
11509 }
11510 )
11511 })
11512 .unwrap();
11513 let label_position = Point2d {
11514 x: Number {
11515 value: 7.0,
11516 units: NumericSuffix::Mm,
11517 },
11518 y: Number {
11519 value: 9.0,
11520 units: NumericSuffix::Mm,
11521 },
11522 };
11523
11524 let (source_delta, scene_delta) = frontend
11525 .edit_segments_with_options(
11526 &mock_ctx,
11527 version,
11528 sketch_id,
11529 vec![ExistingSegmentCtor {
11530 id: line1_id,
11531 ctor: SegmentCtor::Line(LineCtor {
11532 start: point_expr_mm(2.0, 15.55),
11533 end: point_expr_mm(-4.03, 11.51),
11534 construction: None,
11535 }),
11536 }],
11537 EditSegmentsOptions {
11538 anchor_segment_ids: Some(vec![]),
11539 drag_anchors: vec![SegmentDragAnchor {
11540 segment_id: line1_id,
11541 target: label_position.clone(),
11542 }],
11543 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11544 constraint_id,
11545 label_position: label_position.clone(),
11546 }],
11547 commit_solved_initial_guesses: true,
11548 },
11549 )
11550 .await
11551 .unwrap();
11552
11553 assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11554 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11555 let ObjectKind::Constraint {
11556 constraint: Constraint::Distance(distance),
11557 } = &constraint_object.kind
11558 else {
11559 panic!("Expected distance constraint object");
11560 };
11561 assert_eq!(distance.label_position, Some(label_position));
11562
11563 let snapped_label_position = Point2d {
11564 x: Number {
11565 value: 8.0,
11566 units: NumericSuffix::Mm,
11567 },
11568 y: Number {
11569 value: 10.0,
11570 units: NumericSuffix::Mm,
11571 },
11572 };
11573 let (source_delta, scene_delta) = frontend
11574 .edit_segments_with_options(
11575 &mock_ctx,
11576 version,
11577 sketch_id,
11578 vec![],
11579 EditSegmentsOptions {
11580 anchor_segment_ids: Some(vec![line1_id]),
11581 drag_anchors: vec![],
11582 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11583 constraint_id,
11584 label_position: snapped_label_position.clone(),
11585 }],
11586 commit_solved_initial_guesses: true,
11587 },
11588 )
11589 .await
11590 .unwrap();
11591
11592 assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11593 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11594 let ObjectKind::Constraint {
11595 constraint: Constraint::Distance(distance),
11596 } = &constraint_object.kind
11597 else {
11598 panic!("Expected distance constraint object");
11599 };
11600 assert_eq!(distance.label_position, Some(snapped_label_position));
11601
11602 mock_ctx.close().await;
11603 }
11604
11605 #[tokio::test(flavor = "multi_thread")]
11606 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11607 let initial_source = "\
11608sketch(on = XY) {
11609 point1 = point(at = [var 0mm, var 0mm])
11610 point2 = point(at = [var 10mm, var 0mm])
11611 distance([point1, point2]) == 5mm
11612}
11613";
11614
11615 let program = Program::parse(initial_source).unwrap().0.unwrap();
11616 let mut frontend = FrontendState::new();
11617 let mock_ctx = ExecutorContext::new_mock(None).await;
11618 let version = Version(0);
11619
11620 frontend.program = program.clone();
11621 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11622 frontend.update_state_after_exec(outcome, true);
11623 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11624 let sketch_id = sketch_object.id;
11625 let sketch = expect_sketch(sketch_object);
11626 let point0_id = sketch.segments[0];
11627 let point1_id = sketch.segments[1];
11628 let constraint_id = sketch.constraints[0];
11629
11630 let edited_segments = vec![ExistingSegmentCtor {
11631 id: point0_id,
11632 ctor: SegmentCtor::Point(PointCtor {
11633 position: Point2d {
11634 x: Expr::Var(Number {
11635 value: 2.0,
11636 units: NumericSuffix::Mm,
11637 }),
11638 y: Expr::Var(Number {
11639 value: 1.0,
11640 units: NumericSuffix::Mm,
11641 }),
11642 },
11643 }),
11644 }];
11645 let (_, scene_delta) = frontend
11646 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11647 .await
11648 .unwrap();
11649 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11650 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11651
11652 let label_position = Point2d {
11653 x: Number {
11654 value: 3.0,
11655 units: NumericSuffix::Mm,
11656 },
11657 y: Number {
11658 value: 4.0,
11659 units: NumericSuffix::Mm,
11660 },
11661 };
11662 let (_, scene_delta) = frontend
11663 .edit_distance_constraint_label_position(
11664 &mock_ctx,
11665 version,
11666 sketch_id,
11667 constraint_id,
11668 label_position,
11669 vec![point0_id],
11670 )
11671 .await
11672 .unwrap();
11673
11674 assert_point_position_close(
11675 point_position(&scene_delta.new_graph, point0_id),
11676 point0_after_segment_edit,
11677 );
11678 assert_point_position_close(
11679 point_position(&scene_delta.new_graph, point1_id),
11680 point1_after_segment_edit,
11681 );
11682
11683 mock_ctx.close().await;
11684 }
11685
11686 #[tokio::test(flavor = "multi_thread")]
11687 async fn test_distance_point_line() {
11688 let initial_source = "\
11689sketch(on = XY) {
11690 point(at = [var 0, var 5])
11691 line(start = [var 0, var 0], end = [var 10, var 0])
11692}
11693";
11694
11695 let program = Program::parse(initial_source).unwrap().0.unwrap();
11696
11697 let mut frontend = FrontendState::new();
11698
11699 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11700 let mock_ctx = ExecutorContext::new_mock(None).await;
11701 let version = Version(0);
11702
11703 let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
11704 assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
11705 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11706 let sketch_id = sketch_object.id;
11707 let sketch = expect_sketch(sketch_object);
11708 let point_id = *sketch.segments.first().unwrap();
11709 let line_id = *sketch
11710 .segments
11711 .iter()
11712 .find(|segment_id| {
11713 matches!(
11714 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11715 Some(ObjectKind::Segment {
11716 segment: Segment::Line(_)
11717 })
11718 )
11719 })
11720 .unwrap();
11721
11722 let label_position = Point2d {
11723 x: Number {
11724 value: 10.0,
11725 units: NumericSuffix::Mm,
11726 },
11727 y: Number {
11728 value: 11.0,
11729 units: NumericSuffix::Mm,
11730 },
11731 };
11732 let constraint = Constraint::Distance(Distance {
11733 segments: vec![point_id.into(), line_id.into()],
11734 distance: Number {
11735 value: 5.0,
11736 units: NumericSuffix::Mm,
11737 },
11738 label_position: Some(label_position.clone()),
11739 source: Default::default(),
11740 });
11741 let (src_delta, scene_delta) = frontend
11742 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11743 .await
11744 .unwrap();
11745 insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11746 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11747 let sketch = expect_sketch(sketch_object);
11748 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11749 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11750 panic!("Expected constraint object");
11751 };
11752 let Constraint::Distance(distance) = constraint else {
11753 panic!("Expected distance constraint");
11754 };
11755 assert_eq!(distance.label_position, Some(label_position));
11756
11757 ctx.close().await;
11758 mock_ctx.close().await;
11759 }
11760
11761 #[tokio::test(flavor = "multi_thread")]
11762 async fn test_distance_point_arc() {
11763 let initial_source = "\
11764sketch(on = XY) {
11765 point(at = [var 0, var 8])
11766 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11767}
11768";
11769
11770 let program = Program::parse(initial_source).unwrap().0.unwrap();
11771
11772 let mut frontend = FrontendState::new();
11773
11774 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11775 let mock_ctx = ExecutorContext::new_mock(None).await;
11776 let version = Version(0);
11777
11778 frontend.hack_set_program(&ctx, program).await.unwrap();
11779 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11780 let sketch_id = sketch_object.id;
11781 let sketch = expect_sketch(sketch_object);
11782 let point_id = *sketch.segments.first().unwrap();
11783 let arc_id = *sketch
11784 .segments
11785 .iter()
11786 .find(|segment_id| {
11787 matches!(
11788 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11789 Some(ObjectKind::Segment {
11790 segment: Segment::Arc(_)
11791 })
11792 )
11793 })
11794 .unwrap();
11795
11796 let constraint = Constraint::Distance(Distance {
11797 segments: vec![point_id.into(), arc_id.into()],
11798 distance: Number {
11799 value: 3.0,
11800 units: NumericSuffix::Mm,
11801 },
11802 label_position: None,
11803 source: Default::default(),
11804 });
11805 let (src_delta, _scene_delta) = frontend
11806 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11807 .await
11808 .unwrap();
11809 insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11810
11811 ctx.close().await;
11812 mock_ctx.close().await;
11813 }
11814
11815 #[tokio::test(flavor = "multi_thread")]
11816 async fn test_distance_arc_origin() {
11817 let initial_source = "\
11818sketch001 = sketch(on = XY) {
11819 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11820}
11821";
11822
11823 let program = Program::parse(initial_source).unwrap().0.unwrap();
11824
11825 let mut frontend = FrontendState::new();
11826
11827 let mock_ctx = ExecutorContext::new_mock(None).await;
11828 let version = Version(0);
11829
11830 frontend.program = program.clone();
11831 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11832 frontend.update_state_after_exec(outcome, true);
11833 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11834 let sketch_id = sketch_object.id;
11835 let sketch = expect_sketch(sketch_object);
11836 let arc_id = *sketch
11837 .segments
11838 .iter()
11839 .find(|segment_id| {
11840 matches!(
11841 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11842 Some(ObjectKind::Segment {
11843 segment: Segment::Arc(_)
11844 })
11845 )
11846 })
11847 .unwrap();
11848
11849 let constraint = Constraint::Distance(Distance {
11850 segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11851 distance: Number {
11852 value: 3.0,
11853 units: NumericSuffix::Mm,
11854 },
11855 label_position: None,
11856 source: Default::default(),
11857 });
11858 let (src_delta, _scene_delta) = frontend
11859 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11860 .await
11861 .unwrap();
11862 insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11863
11864 mock_ctx.close().await;
11865 }
11866
11867 #[tokio::test(flavor = "multi_thread")]
11868 async fn test_distance_line_origin() {
11869 let initial_source = "\
11870sketch(on = XY) {
11871 line(start = [var 5, var 0], end = [var 5, var 10])
11872}
11873";
11874
11875 let program = Program::parse(initial_source).unwrap().0.unwrap();
11876
11877 let mut frontend = FrontendState::new();
11878
11879 let mock_ctx = ExecutorContext::new_mock(None).await;
11880 let version = Version(0);
11881
11882 frontend.program = program.clone();
11883 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11884 frontend.update_state_after_exec(outcome, true);
11885 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11886 let sketch_id = sketch_object.id;
11887 let sketch = expect_sketch(sketch_object);
11888 let line_id = *sketch
11889 .segments
11890 .iter()
11891 .find(|segment_id| {
11892 matches!(
11893 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11894 Some(ObjectKind::Segment {
11895 segment: Segment::Line(_)
11896 })
11897 )
11898 })
11899 .unwrap();
11900
11901 let constraint = Constraint::Distance(Distance {
11902 segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11903 distance: Number {
11904 value: 5.0,
11905 units: NumericSuffix::Mm,
11906 },
11907 label_position: None,
11908 source: Default::default(),
11909 });
11910 let (src_delta, _scene_delta) = frontend
11911 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11912 .await
11913 .unwrap();
11914 insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11915
11916 mock_ctx.close().await;
11917 }
11918
11919 #[tokio::test(flavor = "multi_thread")]
11920 async fn test_distance_line_circle() {
11921 let initial_source = "\
11922sketch(on = XY) {
11923 line(start = [var -10, var 8], end = [var 10, var 8])
11924 circle(start = [var 5, var 0], center = [var 0, var 0])
11925}
11926";
11927
11928 let program = Program::parse(initial_source).unwrap().0.unwrap();
11929
11930 let mut frontend = FrontendState::new();
11931
11932 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11933 let mock_ctx = ExecutorContext::new_mock(None).await;
11934 let version = Version(0);
11935
11936 frontend.hack_set_program(&ctx, program).await.unwrap();
11937 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11938 let sketch_id = sketch_object.id;
11939 let sketch = expect_sketch(sketch_object);
11940 let line_id = *sketch
11941 .segments
11942 .iter()
11943 .find(|segment_id| {
11944 matches!(
11945 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11946 Some(ObjectKind::Segment {
11947 segment: Segment::Line(_)
11948 })
11949 )
11950 })
11951 .unwrap();
11952 let circle_id = *sketch
11953 .segments
11954 .iter()
11955 .find(|segment_id| {
11956 matches!(
11957 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11958 Some(ObjectKind::Segment {
11959 segment: Segment::Circle(_)
11960 })
11961 )
11962 })
11963 .unwrap();
11964
11965 let constraint = Constraint::Distance(Distance {
11966 segments: vec![line_id.into(), circle_id.into()],
11967 distance: Number {
11968 value: 3.0,
11969 units: NumericSuffix::Mm,
11970 },
11971 label_position: None,
11972 source: Default::default(),
11973 });
11974 let (src_delta, _scene_delta) = frontend
11975 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11976 .await
11977 .unwrap();
11978 insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
11979
11980 ctx.close().await;
11981 mock_ctx.close().await;
11982 }
11983
11984 #[tokio::test(flavor = "multi_thread")]
11985 async fn test_distance_circle_arc() {
11986 let initial_source = "\
11987sketch(on = XY) {
11988 circle(start = [var 5, var 0], center = [var 0, var 0])
11989 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11990}
11991";
11992
11993 let program = Program::parse(initial_source).unwrap().0.unwrap();
11994
11995 let mut frontend = FrontendState::new();
11996
11997 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11998 let mock_ctx = ExecutorContext::new_mock(None).await;
11999 let version = Version(0);
12000
12001 let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
12002 assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
12003 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12004 let sketch_id = sketch_object.id;
12005 let sketch = expect_sketch(sketch_object);
12006 let circle_id = *sketch
12007 .segments
12008 .iter()
12009 .find(|segment_id| {
12010 matches!(
12011 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12012 Some(ObjectKind::Segment {
12013 segment: Segment::Circle(_)
12014 })
12015 )
12016 })
12017 .unwrap();
12018 let arc_id = *sketch
12019 .segments
12020 .iter()
12021 .find(|segment_id| {
12022 matches!(
12023 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12024 Some(ObjectKind::Segment {
12025 segment: Segment::Arc(_)
12026 })
12027 )
12028 })
12029 .unwrap();
12030
12031 let constraint = Constraint::Distance(Distance {
12032 segments: vec![circle_id.into(), arc_id.into()],
12033 distance: Number {
12034 value: 3.0,
12035 units: NumericSuffix::Mm,
12036 },
12037 label_position: None,
12038 source: Default::default(),
12039 });
12040 let (src_delta, _scene_delta) = frontend
12041 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12042 .await
12043 .unwrap();
12044 insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12045
12046 ctx.close().await;
12047 mock_ctx.close().await;
12048 }
12049
12050 #[tokio::test(flavor = "multi_thread")]
12051 async fn test_distance_parallel_lines() {
12052 let initial_source = "\
12053sketch(on = XY) {
12054 line(start = [var 0, var 0], end = [var 10, var 0])
12055 line(start = [var 0, var 5], end = [var 10, var 5])
12056}
12057";
12058
12059 let program = Program::parse(initial_source).unwrap().0.unwrap();
12060
12061 let mut frontend = FrontendState::new();
12062
12063 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12064 let mock_ctx = ExecutorContext::new_mock(None).await;
12065 let version = Version(0);
12066
12067 frontend.hack_set_program(&ctx, program).await.unwrap();
12068 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12069 let sketch_id = sketch_object.id;
12070 let sketch = expect_sketch(sketch_object);
12071 let line_ids = sketch
12072 .segments
12073 .iter()
12074 .copied()
12075 .filter(|segment_id| {
12076 matches!(
12077 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12078 Some(ObjectKind::Segment {
12079 segment: Segment::Line(_)
12080 })
12081 )
12082 })
12083 .collect::<Vec<_>>();
12084
12085 let constraint = Constraint::Distance(Distance {
12086 segments: vec![line_ids[0].into(), line_ids[1].into()],
12087 distance: Number {
12088 value: 5.0,
12089 units: NumericSuffix::Mm,
12090 },
12091 label_position: None,
12092 source: Default::default(),
12093 });
12094 let (src_delta, _scene_delta) = frontend
12095 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12096 .await
12097 .unwrap();
12098 insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12099
12100 ctx.close().await;
12101 mock_ctx.close().await;
12102 }
12103
12104 #[tokio::test(flavor = "multi_thread")]
12105 async fn test_distance_non_parallel_lines_lowers_to_distance() {
12106 let initial_source = "\
12111sketch(on = XY) {
12112 line(start = [var 0, var 0], end = [var 10, var 0])
12113 line(start = [var 0, var 0], end = [var 10, var 10])
12114}
12115";
12116
12117 let program = Program::parse(initial_source).unwrap().0.unwrap();
12118
12119 let mut frontend = FrontendState::new();
12120
12121 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12122 let mock_ctx = ExecutorContext::new_mock(None).await;
12123 let version = Version(0);
12124
12125 frontend.hack_set_program(&ctx, program).await.unwrap();
12126 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12127 let sketch_id = sketch_object.id;
12128 let sketch = expect_sketch(sketch_object);
12129 let line_ids = sketch
12130 .segments
12131 .iter()
12132 .copied()
12133 .filter(|segment_id| {
12134 matches!(
12135 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12136 Some(ObjectKind::Segment {
12137 segment: Segment::Line(_)
12138 })
12139 )
12140 })
12141 .collect::<Vec<_>>();
12142
12143 let constraint = Constraint::Distance(Distance {
12144 segments: vec![line_ids[0].into(), line_ids[1].into()],
12145 distance: Number {
12146 value: 5.0,
12147 units: NumericSuffix::Mm,
12148 },
12149 label_position: None,
12150 source: Default::default(),
12151 });
12152 let (src_delta, _scene_delta) = frontend
12153 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12154 .await
12155 .unwrap();
12156 insta::assert_snapshot!(
12157 "test_distance_non_parallel_lines_lowers_to_distance",
12158 src_delta.text.as_str()
12159 );
12160
12161 ctx.close().await;
12162 mock_ctx.close().await;
12163 }
12164
12165 #[tokio::test(flavor = "multi_thread")]
12166 async fn test_horizontal_distance_two_points() {
12167 let initial_source = "\
12168sketch(on = XY) {
12169 point(at = [var 1, var 2])
12170 point(at = [var 3, var 4])
12171}
12172";
12173
12174 let program = Program::parse(initial_source).unwrap().0.unwrap();
12175
12176 let mut frontend = FrontendState::new();
12177
12178 let mock_ctx = ExecutorContext::new_mock(None).await;
12179 let version = Version(0);
12180
12181 frontend.program = program.clone();
12182 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12183 frontend.update_state_after_exec(outcome, true);
12184 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12185 let sketch_id = sketch_object.id;
12186 let sketch = expect_sketch(sketch_object);
12187 let point0_id = *sketch.segments.first().unwrap();
12188 let point1_id = *sketch.segments.get(1).unwrap();
12189 let label_position = Point2d {
12190 x: Number {
12191 value: 10.0,
12192 units: NumericSuffix::Mm,
12193 },
12194 y: Number {
12195 value: 11.0,
12196 units: NumericSuffix::Mm,
12197 },
12198 };
12199
12200 let constraint = Constraint::HorizontalDistance(Distance {
12201 segments: vec![point0_id.into(), point1_id.into()],
12202 distance: Number {
12203 value: 2.0,
12204 units: NumericSuffix::Mm,
12205 },
12206 label_position: Some(label_position.clone()),
12207 source: Default::default(),
12208 });
12209 let (src_delta, scene_delta) = frontend
12210 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12211 .await
12212 .unwrap();
12213 insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12214 assert_eq!(
12215 scene_delta.new_graph.objects.len(),
12216 5,
12217 "{:#?}",
12218 scene_delta.new_graph.objects
12219 );
12220 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12221 let sketch = expect_sketch(sketch_object);
12222 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12223 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12224 panic!("Expected constraint object");
12225 };
12226 let Constraint::HorizontalDistance(distance) = constraint else {
12227 panic!("Expected horizontal distance constraint");
12228 };
12229 assert_eq!(distance.label_position, Some(label_position));
12230
12231 mock_ctx.close().await;
12232 }
12233
12234 #[tokio::test(flavor = "multi_thread")]
12235 async fn test_radius_single_arc_segment() {
12236 let initial_source = "\
12237sketch(on = XY) {
12238 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12239}
12240";
12241
12242 let program = Program::parse(initial_source).unwrap().0.unwrap();
12243
12244 let mut frontend = FrontendState::new();
12245
12246 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12247 let mock_ctx = ExecutorContext::new_mock(None).await;
12248 let version = Version(0);
12249
12250 frontend.hack_set_program(&ctx, program).await.unwrap();
12251 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12252 let sketch_id = sketch_object.id;
12253 let sketch = expect_sketch(sketch_object);
12254 let arc_id = sketch
12256 .segments
12257 .iter()
12258 .find(|&seg_id| {
12259 let obj = frontend.scene_graph.objects.get(seg_id.0);
12260 matches!(
12261 obj.map(|o| &o.kind),
12262 Some(ObjectKind::Segment {
12263 segment: Segment::Arc(_)
12264 })
12265 )
12266 })
12267 .unwrap();
12268
12269 let constraint = Constraint::Radius(Radius {
12270 arc: *arc_id,
12271 radius: Number {
12272 value: 5.0,
12273 units: NumericSuffix::Mm,
12274 },
12275 label_position: None,
12276 source: Default::default(),
12277 });
12278 let (src_delta, scene_delta) = frontend
12279 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12280 .await
12281 .unwrap();
12282 insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12283 assert_eq!(
12284 scene_delta.new_graph.objects.len(),
12285 7, "{:#?}",
12287 scene_delta.new_graph.objects
12288 );
12289
12290 ctx.close().await;
12291 mock_ctx.close().await;
12292 }
12293
12294 #[tokio::test(flavor = "multi_thread")]
12295 async fn test_radius_single_arc_segment_with_label_position() {
12296 let initial_source = "\
12297sketch(on = XY) {
12298 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12299}
12300";
12301
12302 let program = Program::parse(initial_source).unwrap().0.unwrap();
12303 let mut frontend = FrontendState::new();
12304 let mock_ctx = ExecutorContext::new_mock(None).await;
12305 let version = Version(0);
12306
12307 frontend.program = program.clone();
12308 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12309 frontend.update_state_after_exec(outcome, true);
12310 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12311 let sketch_id = sketch_object.id;
12312 let sketch = expect_sketch(sketch_object);
12313 let arc_id = sketch
12314 .segments
12315 .iter()
12316 .find(|&seg_id| {
12317 let obj = frontend.scene_graph.objects.get(seg_id.0);
12318 matches!(
12319 obj.map(|o| &o.kind),
12320 Some(ObjectKind::Segment {
12321 segment: Segment::Arc(_)
12322 })
12323 )
12324 })
12325 .unwrap();
12326
12327 let label_position = Point2d {
12328 x: Number {
12329 value: 10.0,
12330 units: NumericSuffix::Mm,
12331 },
12332 y: Number {
12333 value: 11.0,
12334 units: NumericSuffix::Mm,
12335 },
12336 };
12337 let constraint = Constraint::Radius(Radius {
12338 arc: *arc_id,
12339 radius: Number {
12340 value: 5.0,
12341 units: NumericSuffix::Mm,
12342 },
12343 label_position: Some(label_position.clone()),
12344 source: Default::default(),
12345 });
12346 let (src_delta, scene_delta) = frontend
12347 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12348 .await
12349 .unwrap();
12350 insta::assert_snapshot!(
12351 "test_radius_single_arc_segment_with_label_position",
12352 src_delta.text.as_str()
12353 );
12354
12355 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12356 let sketch = expect_sketch(sketch_object);
12357 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12358 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12359 panic!("Expected constraint object");
12360 };
12361 let Constraint::Radius(radius) = constraint else {
12362 panic!("Expected radius constraint");
12363 };
12364 assert_eq!(radius.label_position, Some(label_position));
12365
12366 mock_ctx.close().await;
12367 }
12368
12369 #[tokio::test(flavor = "multi_thread")]
12370 async fn test_edit_radius_constraint_label_position() {
12371 let initial_source = "\
12372sketch(on = XY) {
12373 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12374 radius(arc1) == 5mm
12375}
12376";
12377
12378 let program = Program::parse(initial_source).unwrap().0.unwrap();
12379 let mut frontend = FrontendState::new();
12380 let mock_ctx = ExecutorContext::new_mock(None).await;
12381 let version = Version(0);
12382
12383 frontend.program = program.clone();
12384 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12385 frontend.update_state_after_exec(outcome, true);
12386 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12387 let sketch_id = sketch_object.id;
12388 let sketch = expect_sketch(sketch_object);
12389 let constraint_id = sketch.constraints[0];
12390 let label_position = Point2d {
12391 x: Number {
12392 value: 10.0,
12393 units: NumericSuffix::Mm,
12394 },
12395 y: Number {
12396 value: 11.0,
12397 units: NumericSuffix::Mm,
12398 },
12399 };
12400
12401 let (src_delta, scene_delta) = frontend
12402 .edit_distance_constraint_label_position(
12403 &mock_ctx,
12404 version,
12405 sketch_id,
12406 constraint_id,
12407 label_position.clone(),
12408 vec![],
12409 )
12410 .await
12411 .unwrap();
12412 insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12413
12414 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12415 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12416 panic!("Expected constraint object");
12417 };
12418 let Constraint::Radius(radius) = constraint else {
12419 panic!("Expected radius constraint");
12420 };
12421 assert_eq!(radius.label_position, Some(label_position));
12422
12423 mock_ctx.close().await;
12424 }
12425
12426 #[tokio::test(flavor = "multi_thread")]
12427 async fn test_vertical_distance_two_points() {
12428 let initial_source = "\
12429sketch(on = XY) {
12430 point(at = [var 1, var 2])
12431 point(at = [var 3, var 4])
12432}
12433";
12434
12435 let program = Program::parse(initial_source).unwrap().0.unwrap();
12436
12437 let mut frontend = FrontendState::new();
12438
12439 let mock_ctx = ExecutorContext::new_mock(None).await;
12440 let version = Version(0);
12441
12442 frontend.program = program.clone();
12443 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12444 frontend.update_state_after_exec(outcome, true);
12445 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12446 let sketch_id = sketch_object.id;
12447 let sketch = expect_sketch(sketch_object);
12448 let point0_id = *sketch.segments.first().unwrap();
12449 let point1_id = *sketch.segments.get(1).unwrap();
12450 let label_position = Point2d {
12451 x: Number {
12452 value: 10.0,
12453 units: NumericSuffix::Mm,
12454 },
12455 y: Number {
12456 value: 11.0,
12457 units: NumericSuffix::Mm,
12458 },
12459 };
12460
12461 let constraint = Constraint::VerticalDistance(Distance {
12462 segments: vec![point0_id.into(), point1_id.into()],
12463 distance: Number {
12464 value: 2.0,
12465 units: NumericSuffix::Mm,
12466 },
12467 label_position: Some(label_position.clone()),
12468 source: Default::default(),
12469 });
12470 let (src_delta, scene_delta) = frontend
12471 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12472 .await
12473 .unwrap();
12474 insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12475 assert_eq!(
12476 scene_delta.new_graph.objects.len(),
12477 5,
12478 "{:#?}",
12479 scene_delta.new_graph.objects
12480 );
12481 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12482 let sketch = expect_sketch(sketch_object);
12483 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12484 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12485 panic!("Expected constraint object");
12486 };
12487 let Constraint::VerticalDistance(distance) = constraint else {
12488 panic!("Expected vertical distance constraint");
12489 };
12490 assert_eq!(distance.label_position, Some(label_position));
12491
12492 mock_ctx.close().await;
12493 }
12494
12495 #[tokio::test(flavor = "multi_thread")]
12496 async fn test_add_fixed_standalone_point() {
12497 let initial_source = "\
12498sketch(on = XY) {
12499 point(at = [var 1, var 2])
12500}
12501";
12502
12503 let program = Program::parse(initial_source).unwrap().0.unwrap();
12504
12505 let mut frontend = FrontendState::new();
12506
12507 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12508 let mock_ctx = ExecutorContext::new_mock(None).await;
12509 let version = Version(0);
12510
12511 frontend.hack_set_program(&ctx, program).await.unwrap();
12512 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12513 let sketch_id = sketch_object.id;
12514 let sketch = expect_sketch(sketch_object);
12515 let point_id = *sketch.segments.first().unwrap();
12516
12517 let (src_delta, scene_delta) = frontend
12518 .add_constraint(
12519 &mock_ctx,
12520 version,
12521 sketch_id,
12522 Constraint::Fixed(Fixed {
12523 points: vec![FixedPoint {
12524 point: point_id,
12525 position: Point2d {
12526 x: Number {
12527 value: 2.0,
12528 units: NumericSuffix::Mm,
12529 },
12530 y: Number {
12531 value: 3.0,
12532 units: NumericSuffix::Mm,
12533 },
12534 },
12535 }],
12536 }),
12537 )
12538 .await
12539 .unwrap();
12540 insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12541 assert_eq!(
12542 scene_delta.new_graph.objects.len(),
12543 4,
12544 "{:#?}",
12545 scene_delta.new_graph.objects
12546 );
12547
12548 ctx.close().await;
12549 mock_ctx.close().await;
12550 }
12551
12552 #[tokio::test(flavor = "multi_thread")]
12553 async fn test_add_fixed_multiple_points() {
12554 let initial_source = "\
12555sketch(on = XY) {
12556 point(at = [var 1, var 2])
12557 point(at = [var 3, var 4])
12558}
12559";
12560
12561 let program = Program::parse(initial_source).unwrap().0.unwrap();
12562
12563 let mut frontend = FrontendState::new();
12564
12565 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12566 let mock_ctx = ExecutorContext::new_mock(None).await;
12567 let version = Version(0);
12568
12569 frontend.hack_set_program(&ctx, program).await.unwrap();
12570 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12571 let sketch_id = sketch_object.id;
12572 let sketch = expect_sketch(sketch_object);
12573 let point0_id = *sketch.segments.first().unwrap();
12574 let point1_id = *sketch.segments.get(1).unwrap();
12575
12576 let (src_delta, scene_delta) = frontend
12577 .add_constraint(
12578 &mock_ctx,
12579 version,
12580 sketch_id,
12581 Constraint::Fixed(Fixed {
12582 points: vec![
12583 FixedPoint {
12584 point: point0_id,
12585 position: Point2d {
12586 x: Number {
12587 value: 2.0,
12588 units: NumericSuffix::Mm,
12589 },
12590 y: Number {
12591 value: 3.0,
12592 units: NumericSuffix::Mm,
12593 },
12594 },
12595 },
12596 FixedPoint {
12597 point: point1_id,
12598 position: Point2d {
12599 x: Number {
12600 value: 4.0,
12601 units: NumericSuffix::Mm,
12602 },
12603 y: Number {
12604 value: 5.0,
12605 units: NumericSuffix::Mm,
12606 },
12607 },
12608 },
12609 ],
12610 }),
12611 )
12612 .await
12613 .unwrap();
12614 insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12615 assert_eq!(
12616 scene_delta.new_graph.objects.len(),
12617 6,
12618 "{:#?}",
12619 scene_delta.new_graph.objects
12620 );
12621
12622 ctx.close().await;
12623 mock_ctx.close().await;
12624 }
12625
12626 #[tokio::test(flavor = "multi_thread")]
12627 async fn test_add_fixed_owned_point() {
12628 let initial_source = "\
12629sketch(on = XY) {
12630 line(start = [var 1, var 2], end = [var 3, var 4])
12631}
12632";
12633
12634 let program = Program::parse(initial_source).unwrap().0.unwrap();
12635
12636 let mut frontend = FrontendState::new();
12637
12638 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12639 let mock_ctx = ExecutorContext::new_mock(None).await;
12640 let version = Version(0);
12641
12642 frontend.hack_set_program(&ctx, program).await.unwrap();
12643 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12644 let sketch_id = sketch_object.id;
12645 let sketch = expect_sketch(sketch_object);
12646 let line_start_id = *sketch.segments.first().unwrap();
12647
12648 let (src_delta, scene_delta) = frontend
12649 .add_constraint(
12650 &mock_ctx,
12651 version,
12652 sketch_id,
12653 Constraint::Fixed(Fixed {
12654 points: vec![FixedPoint {
12655 point: line_start_id,
12656 position: Point2d {
12657 x: Number {
12658 value: 2.0,
12659 units: NumericSuffix::Mm,
12660 },
12661 y: Number {
12662 value: 3.0,
12663 units: NumericSuffix::Mm,
12664 },
12665 },
12666 }],
12667 }),
12668 )
12669 .await
12670 .unwrap();
12671 insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12672 assert_eq!(
12673 scene_delta.new_graph.objects.len(),
12674 6,
12675 "{:#?}",
12676 scene_delta.new_graph.objects
12677 );
12678
12679 ctx.close().await;
12680 mock_ctx.close().await;
12681 }
12682
12683 #[tokio::test(flavor = "multi_thread")]
12684 async fn test_radius_error_cases() {
12685 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12686 let mock_ctx = ExecutorContext::new_mock(None).await;
12687 let version = Version(0);
12688
12689 let initial_source_point = "\
12691sketch(on = XY) {
12692 point(at = [var 1, var 2])
12693}
12694";
12695 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12696 let mut frontend_point = FrontendState::new();
12697 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12698 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12699 let sketch_id_point = sketch_object_point.id;
12700 let sketch_point = expect_sketch(sketch_object_point);
12701 let point_id = *sketch_point.segments.first().unwrap();
12702
12703 let constraint_point = Constraint::Radius(Radius {
12704 arc: point_id,
12705 radius: Number {
12706 value: 5.0,
12707 units: NumericSuffix::Mm,
12708 },
12709 label_position: None,
12710 source: Default::default(),
12711 });
12712 let result_point = frontend_point
12713 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12714 .await;
12715 assert!(result_point.is_err(), "Single point should error for radius");
12716
12717 let initial_source_line = "\
12719sketch(on = XY) {
12720 line(start = [var 1, var 2], end = [var 3, var 4])
12721}
12722";
12723 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12724 let mut frontend_line = FrontendState::new();
12725 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12726 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12727 let sketch_id_line = sketch_object_line.id;
12728 let sketch_line = expect_sketch(sketch_object_line);
12729 let line_id = *sketch_line.segments.first().unwrap();
12730
12731 let constraint_line = Constraint::Radius(Radius {
12732 arc: line_id,
12733 radius: Number {
12734 value: 5.0,
12735 units: NumericSuffix::Mm,
12736 },
12737 label_position: None,
12738 source: Default::default(),
12739 });
12740 let result_line = frontend_line
12741 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12742 .await;
12743 assert!(result_line.is_err(), "Single line segment should error for radius");
12744
12745 ctx.close().await;
12746 mock_ctx.close().await;
12747 }
12748
12749 #[tokio::test(flavor = "multi_thread")]
12750 async fn test_diameter_single_arc_segment() {
12751 let initial_source = "\
12752sketch(on = XY) {
12753 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12754}
12755";
12756
12757 let program = Program::parse(initial_source).unwrap().0.unwrap();
12758
12759 let mut frontend = FrontendState::new();
12760
12761 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12762 let mock_ctx = ExecutorContext::new_mock(None).await;
12763 let version = Version(0);
12764
12765 frontend.hack_set_program(&ctx, program).await.unwrap();
12766 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12767 let sketch_id = sketch_object.id;
12768 let sketch = expect_sketch(sketch_object);
12769 let arc_id = sketch
12771 .segments
12772 .iter()
12773 .find(|&seg_id| {
12774 let obj = frontend.scene_graph.objects.get(seg_id.0);
12775 matches!(
12776 obj.map(|o| &o.kind),
12777 Some(ObjectKind::Segment {
12778 segment: Segment::Arc(_)
12779 })
12780 )
12781 })
12782 .unwrap();
12783
12784 let constraint = Constraint::Diameter(Diameter {
12785 arc: *arc_id,
12786 diameter: Number {
12787 value: 10.0,
12788 units: NumericSuffix::Mm,
12789 },
12790 label_position: None,
12791 source: Default::default(),
12792 });
12793 let (src_delta, scene_delta) = frontend
12794 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12795 .await
12796 .unwrap();
12797 insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12798 assert_eq!(
12799 scene_delta.new_graph.objects.len(),
12800 7, "{:#?}",
12802 scene_delta.new_graph.objects
12803 );
12804
12805 ctx.close().await;
12806 mock_ctx.close().await;
12807 }
12808
12809 #[tokio::test(flavor = "multi_thread")]
12810 async fn test_diameter_single_arc_segment_with_label_position() {
12811 let initial_source = "\
12812sketch(on = XY) {
12813 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12814}
12815";
12816
12817 let program = Program::parse(initial_source).unwrap().0.unwrap();
12818 let mut frontend = FrontendState::new();
12819 let mock_ctx = ExecutorContext::new_mock(None).await;
12820 let version = Version(0);
12821
12822 frontend.program = program.clone();
12823 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12824 frontend.update_state_after_exec(outcome, true);
12825 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12826 let sketch_id = sketch_object.id;
12827 let sketch = expect_sketch(sketch_object);
12828 let arc_id = sketch
12829 .segments
12830 .iter()
12831 .find(|&seg_id| {
12832 let obj = frontend.scene_graph.objects.get(seg_id.0);
12833 matches!(
12834 obj.map(|o| &o.kind),
12835 Some(ObjectKind::Segment {
12836 segment: Segment::Arc(_)
12837 })
12838 )
12839 })
12840 .unwrap();
12841
12842 let label_position = Point2d {
12843 x: Number {
12844 value: 10.0,
12845 units: NumericSuffix::Mm,
12846 },
12847 y: Number {
12848 value: 11.0,
12849 units: NumericSuffix::Mm,
12850 },
12851 };
12852 let constraint = Constraint::Diameter(Diameter {
12853 arc: *arc_id,
12854 diameter: Number {
12855 value: 10.0,
12856 units: NumericSuffix::Mm,
12857 },
12858 label_position: Some(label_position.clone()),
12859 source: Default::default(),
12860 });
12861 let (src_delta, scene_delta) = frontend
12862 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12863 .await
12864 .unwrap();
12865 insta::assert_snapshot!(
12866 "test_diameter_single_arc_segment_with_label_position",
12867 src_delta.text.as_str()
12868 );
12869
12870 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12871 let sketch = expect_sketch(sketch_object);
12872 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12873 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12874 panic!("Expected constraint object");
12875 };
12876 let Constraint::Diameter(diameter) = constraint else {
12877 panic!("Expected diameter constraint");
12878 };
12879 assert_eq!(diameter.label_position, Some(label_position));
12880
12881 mock_ctx.close().await;
12882 }
12883
12884 #[tokio::test(flavor = "multi_thread")]
12885 async fn test_edit_diameter_constraint_label_position() {
12886 let initial_source = "\
12887sketch(on = XY) {
12888 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12889 diameter(arc1) == 10mm
12890}
12891";
12892
12893 let program = Program::parse(initial_source).unwrap().0.unwrap();
12894 let mut frontend = FrontendState::new();
12895 let mock_ctx = ExecutorContext::new_mock(None).await;
12896 let version = Version(0);
12897
12898 frontend.program = program.clone();
12899 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12900 frontend.update_state_after_exec(outcome, true);
12901 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12902 let sketch_id = sketch_object.id;
12903 let sketch = expect_sketch(sketch_object);
12904 let constraint_id = sketch.constraints[0];
12905 let label_position = Point2d {
12906 x: Number {
12907 value: 10.0,
12908 units: NumericSuffix::Mm,
12909 },
12910 y: Number {
12911 value: 11.0,
12912 units: NumericSuffix::Mm,
12913 },
12914 };
12915
12916 let (src_delta, scene_delta) = frontend
12917 .edit_distance_constraint_label_position(
12918 &mock_ctx,
12919 version,
12920 sketch_id,
12921 constraint_id,
12922 label_position.clone(),
12923 vec![],
12924 )
12925 .await
12926 .unwrap();
12927 insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12928
12929 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12930 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12931 panic!("Expected constraint object");
12932 };
12933 let Constraint::Diameter(diameter) = constraint else {
12934 panic!("Expected diameter constraint");
12935 };
12936 assert_eq!(diameter.label_position, Some(label_position));
12937
12938 mock_ctx.close().await;
12939 }
12940
12941 #[tokio::test(flavor = "multi_thread")]
12942 async fn test_diameter_error_cases() {
12943 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12944 let mock_ctx = ExecutorContext::new_mock(None).await;
12945 let version = Version(0);
12946
12947 let initial_source_point = "\
12949sketch(on = XY) {
12950 point(at = [var 1, var 2])
12951}
12952";
12953 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12954 let mut frontend_point = FrontendState::new();
12955 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12956 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12957 let sketch_id_point = sketch_object_point.id;
12958 let sketch_point = expect_sketch(sketch_object_point);
12959 let point_id = *sketch_point.segments.first().unwrap();
12960
12961 let constraint_point = Constraint::Diameter(Diameter {
12962 arc: point_id,
12963 diameter: Number {
12964 value: 10.0,
12965 units: NumericSuffix::Mm,
12966 },
12967 label_position: None,
12968 source: Default::default(),
12969 });
12970 let result_point = frontend_point
12971 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12972 .await;
12973 assert!(result_point.is_err(), "Single point should error for diameter");
12974
12975 let initial_source_line = "\
12977sketch(on = XY) {
12978 line(start = [var 1, var 2], end = [var 3, var 4])
12979}
12980";
12981 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12982 let mut frontend_line = FrontendState::new();
12983 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12984 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12985 let sketch_id_line = sketch_object_line.id;
12986 let sketch_line = expect_sketch(sketch_object_line);
12987 let line_id = *sketch_line.segments.first().unwrap();
12988
12989 let constraint_line = Constraint::Diameter(Diameter {
12990 arc: line_id,
12991 diameter: Number {
12992 value: 10.0,
12993 units: NumericSuffix::Mm,
12994 },
12995 label_position: None,
12996 source: Default::default(),
12997 });
12998 let result_line = frontend_line
12999 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
13000 .await;
13001 assert!(result_line.is_err(), "Single line segment should error for diameter");
13002
13003 ctx.close().await;
13004 mock_ctx.close().await;
13005 }
13006
13007 #[tokio::test(flavor = "multi_thread")]
13008 async fn test_line_horizontal() {
13009 let initial_source = "\
13010sketch(on = XY) {
13011 line(start = [var 1, var 2], end = [var 3, var 4])
13012}
13013";
13014
13015 let program = Program::parse(initial_source).unwrap().0.unwrap();
13016
13017 let mut frontend = FrontendState::new();
13018
13019 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13020 let mock_ctx = ExecutorContext::new_mock(None).await;
13021 let version = Version(0);
13022
13023 frontend.hack_set_program(&ctx, program).await.unwrap();
13024 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13025 let sketch_id = sketch_object.id;
13026 let sketch = expect_sketch(sketch_object);
13027 let line1_id = *sketch.segments.get(2).unwrap();
13028
13029 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13030 let (src_delta, scene_delta) = frontend
13031 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13032 .await
13033 .unwrap();
13034 insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13035 assert_eq!(
13036 scene_delta.new_graph.objects.len(),
13037 6,
13038 "{:#?}",
13039 scene_delta.new_graph.objects
13040 );
13041
13042 ctx.close().await;
13043 mock_ctx.close().await;
13044 }
13045
13046 #[tokio::test(flavor = "multi_thread")]
13047 async fn test_control_point_spline_edge_horizontal() {
13048 let initial_source = "\
13049@settings(experimentalFeatures = allow)
13050splineSketch = sketch(on = XY) {
13051 controlPointSpline1 = controlPointSpline(points = [
13052 [var 0mm, var 0mm],
13053 [var 10mm, var 20mm],
13054 [var 20mm, var 0mm],
13055 ])
13056}
13057";
13058
13059 let program = Program::parse(initial_source).unwrap().0.unwrap();
13060
13061 let mut frontend = FrontendState::new();
13062
13063 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13064 let mock_ctx = ExecutorContext::new_mock(None).await;
13065 let version = Version(0);
13066
13067 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13068 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13069 let sketch_id = sketch_object.id;
13070 let sketch = expect_sketch(sketch_object);
13071 let spline_id = sketch
13072 .segments
13073 .iter()
13074 .copied()
13075 .find(|seg_id| {
13076 matches!(
13077 &frontend.scene_graph.objects[seg_id.0].kind,
13078 ObjectKind::Segment {
13079 segment: Segment::ControlPointSpline(_)
13080 }
13081 )
13082 })
13083 .expect("Expected a control point spline segment in sketch");
13084 let edge_id = frontend
13085 .scene_graph
13086 .objects
13087 .iter()
13088 .find_map(|obj| match &obj.kind {
13089 ObjectKind::Segment {
13090 segment: Segment::Line(line),
13091 } if line.owner == Some(spline_id) => Some(obj.id),
13092 _ => None,
13093 })
13094 .expect("Expected an owned control-polygon edge");
13095
13096 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13097 let (src_delta, _) = frontend
13098 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13099 .await
13100 .unwrap();
13101 assert!(
13102 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13103 "Expected horizontal constraint on spline edge, got: {}",
13104 src_delta.text
13105 );
13106
13107 ctx.close().await;
13108 mock_ctx.close().await;
13109 }
13110
13111 #[tokio::test(flavor = "multi_thread")]
13112 async fn test_control_point_spline_edge_angle() {
13113 let initial_source = "\
13114@settings(experimentalFeatures = allow)
13115splineSketch = sketch(on = XY) {
13116 controlPointSpline1 = controlPointSpline(points = [
13117 [var 0mm, var 0mm],
13118 [var 10mm, var 20mm],
13119 [var 20mm, var 0mm],
13120 ])
13121
13122 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13123}
13124";
13125
13126 let program = Program::parse(initial_source).unwrap().0.unwrap();
13127
13128 let mut frontend = FrontendState::new();
13129
13130 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13131 let mock_ctx = ExecutorContext::new_mock(None).await;
13132 let version = Version(0);
13133
13134 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13135 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13136 let sketch_id = sketch_object.id;
13137 let sketch = expect_sketch(sketch_object);
13138 let spline_id = sketch
13139 .segments
13140 .iter()
13141 .copied()
13142 .find(|seg_id| {
13143 matches!(
13144 &frontend.scene_graph.objects[seg_id.0].kind,
13145 ObjectKind::Segment {
13146 segment: Segment::ControlPointSpline(_)
13147 }
13148 )
13149 })
13150 .expect("Expected a control point spline segment in sketch");
13151 let edge_id = frontend
13152 .scene_graph
13153 .objects
13154 .iter()
13155 .find_map(|obj| match &obj.kind {
13156 ObjectKind::Segment {
13157 segment: Segment::Line(line),
13158 } if line.owner == Some(spline_id) => Some(obj.id),
13159 _ => None,
13160 })
13161 .expect("Expected an owned control-polygon edge");
13162 let line1_id = frontend
13163 .scene_graph
13164 .objects
13165 .iter()
13166 .find_map(|obj| match &obj.kind {
13167 ObjectKind::Segment {
13168 segment: Segment::Line(line),
13169 } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
13170 _ => None,
13171 })
13172 .or_else(|| {
13173 sketch.segments.iter().copied().find(|seg_id| {
13174 matches!(
13175 &frontend.scene_graph.objects[seg_id.0].kind,
13176 ObjectKind::Segment {
13177 segment: Segment::Line(line),
13178 } if line.owner.is_none()
13179 )
13180 })
13181 })
13182 .expect("Expected a standalone line segment in sketch");
13183
13184 let constraint = Constraint::Angle(Angle {
13185 lines: vec![line1_id, edge_id],
13186 angle: Number {
13187 value: 30.0,
13188 units: NumericSuffix::Deg,
13189 },
13190 sector: None,
13191 inverse: None,
13192 label_position: None,
13193 source: Default::default(),
13194 });
13195 let (src_delta, _) = frontend
13196 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13197 .await
13198 .unwrap();
13199 assert!(
13200 src_delta
13201 .text
13202 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13203 "Expected angle constraint on spline edge, got: {}",
13204 src_delta.text
13205 );
13206
13207 ctx.close().await;
13208 mock_ctx.close().await;
13209 }
13210
13211 #[tokio::test(flavor = "multi_thread")]
13212 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13213 let initial_source = "\
13214@settings(experimentalFeatures = allow)
13215splineSketch = sketch(on = XY) {
13216 spline1 = controlPointSpline(points = [
13217 [var 0mm, var 0mm],
13218 [var 10mm, var 20mm],
13219 [var 20mm, var 0mm],
13220 ])
13221 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13222 coincident([spline1.controls[1], spline1.edges[0]])
13223 coincident([spline1.controls[0], line1])
13224}
13225";
13226
13227 let program = Program::parse(initial_source).unwrap().0.unwrap();
13228
13229 let mut frontend = FrontendState::new();
13230
13231 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13232 let mock_ctx = ExecutorContext::new_mock(None).await;
13233
13234 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13235
13236 let ui_scene_graph = frontend.scene_graph_for_ui();
13237 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13238 let sketch = expect_sketch(sketch_object);
13239
13240 assert_eq!(
13241 sketch.constraints.len(),
13242 1,
13243 "Expected only the external coincident constraint to remain visible in the UI scene graph"
13244 );
13245
13246 let visible_constraints = ui_scene_graph
13247 .objects
13248 .iter()
13249 .filter_map(|object| match &object.kind {
13250 ObjectKind::Constraint {
13251 constraint: Constraint::Coincident(coincident),
13252 } => Some(coincident.clone()),
13253 _ => None,
13254 })
13255 .collect::<Vec<_>>();
13256
13257 assert_eq!(
13258 visible_constraints.len(),
13259 1,
13260 "Expected only one coincident constraint object in the UI scene graph"
13261 );
13262 assert_eq!(
13263 visible_constraints[0].get_segments().len(),
13264 2,
13265 "Expected the remaining visible coincident constraint to reference two segments"
13266 );
13267
13268 ctx.close().await;
13269 mock_ctx.close().await;
13270 }
13271
13272 #[tokio::test(flavor = "multi_thread")]
13273 async fn test_edit_control_point_spline_can_append_control_point() {
13274 let initial_source = "\
13275@settings(experimentalFeatures = allow)
13276splineSketch = sketch(on = XY) {
13277 controlPointSpline(points = [
13278 [var 0mm, var 0mm],
13279 [var 10mm, var 20mm],
13280 [var 20mm, var 0mm],
13281 ])
13282}
13283";
13284
13285 let program = Program::parse(initial_source).unwrap().0.unwrap();
13286
13287 let mut frontend = FrontendState::new();
13288
13289 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13290 let mock_ctx = ExecutorContext::new_mock(None).await;
13291 let version = Version(0);
13292
13293 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13294 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13295 let sketch_id = sketch_object.id;
13296 let sketch = expect_sketch(sketch_object);
13297 let spline_id = sketch
13298 .segments
13299 .iter()
13300 .copied()
13301 .find(|seg_id| {
13302 matches!(
13303 &frontend.scene_graph.objects[seg_id.0].kind,
13304 ObjectKind::Segment {
13305 segment: Segment::ControlPointSpline(_)
13306 }
13307 )
13308 })
13309 .expect("Expected a control point spline segment in sketch");
13310
13311 let ctor = ControlPointSplineCtor {
13312 points: vec![
13313 Point2d {
13314 x: Expr::Var(Number {
13315 value: 0.0,
13316 units: NumericSuffix::Mm,
13317 }),
13318 y: Expr::Var(Number {
13319 value: 0.0,
13320 units: NumericSuffix::Mm,
13321 }),
13322 },
13323 Point2d {
13324 x: Expr::Var(Number {
13325 value: 10.0,
13326 units: NumericSuffix::Mm,
13327 }),
13328 y: Expr::Var(Number {
13329 value: 20.0,
13330 units: NumericSuffix::Mm,
13331 }),
13332 },
13333 Point2d {
13334 x: Expr::Var(Number {
13335 value: 20.0,
13336 units: NumericSuffix::Mm,
13337 }),
13338 y: Expr::Var(Number {
13339 value: 0.0,
13340 units: NumericSuffix::Mm,
13341 }),
13342 },
13343 Point2d {
13344 x: Expr::Var(Number {
13345 value: 30.0,
13346 units: NumericSuffix::Mm,
13347 }),
13348 y: Expr::Var(Number {
13349 value: 10.0,
13350 units: NumericSuffix::Mm,
13351 }),
13352 },
13353 ],
13354 construction: None,
13355 };
13356
13357 let segments = vec![ExistingSegmentCtor {
13358 id: spline_id,
13359 ctor: SegmentCtor::ControlPointSpline(ctor),
13360 }];
13361 let (src_delta, scene_delta) = frontend
13362 .edit_segments(&mock_ctx, version, sketch_id, segments)
13363 .await
13364 .unwrap();
13365
13366 assert!(
13367 src_delta.text.contains("[var 30mm, var 10mm]"),
13368 "Expected appended spline control point in source, got: {}",
13369 src_delta.text
13370 );
13371
13372 assert!(
13373 scene_delta.invalidates_ids,
13374 "Expected appending a spline control point to invalidate ids"
13375 );
13376 let updated_spline = scene_delta
13377 .new_graph
13378 .objects
13379 .iter()
13380 .find_map(|obj| match &obj.kind {
13381 ObjectKind::Segment {
13382 segment: Segment::ControlPointSpline(updated_spline),
13383 } if updated_spline.controls.len() == 4 => Some(updated_spline),
13384 _ => None,
13385 })
13386 .expect("Expected edited scene graph to contain a four-point control point spline");
13387 assert_eq!(
13388 updated_spline.controls.len(),
13389 4,
13390 "Expected edited spline to expose four control points"
13391 );
13392
13393 ctx.close().await;
13394 mock_ctx.close().await;
13395 }
13396
13397 #[tokio::test(flavor = "multi_thread")]
13398 async fn test_line_vertical() {
13399 let initial_source = "\
13400sketch(on = XY) {
13401 line(start = [var 1, var 2], end = [var 3, var 4])
13402}
13403";
13404
13405 let program = Program::parse(initial_source).unwrap().0.unwrap();
13406
13407 let mut frontend = FrontendState::new();
13408
13409 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13410 let mock_ctx = ExecutorContext::new_mock(None).await;
13411 let version = Version(0);
13412
13413 frontend.hack_set_program(&ctx, program).await.unwrap();
13414 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13415 let sketch_id = sketch_object.id;
13416 let sketch = expect_sketch(sketch_object);
13417 let line1_id = *sketch.segments.get(2).unwrap();
13418
13419 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13420 let (src_delta, scene_delta) = frontend
13421 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13422 .await
13423 .unwrap();
13424 insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13425 assert_eq!(
13426 scene_delta.new_graph.objects.len(),
13427 6,
13428 "{:#?}",
13429 scene_delta.new_graph.objects
13430 );
13431
13432 ctx.close().await;
13433 mock_ctx.close().await;
13434 }
13435
13436 #[tokio::test(flavor = "multi_thread")]
13437 async fn test_points_vertical() {
13438 let initial_source = "\
13439sketch001 = sketch(on = XY) {
13440 p0 = point(at = [var -2.23mm, var 3.1mm])
13441 pf = point(at = [4, 4])
13442}
13443";
13444
13445 let program = Program::parse(initial_source).unwrap().0.unwrap();
13446
13447 let mut frontend = FrontendState::new();
13448
13449 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13450 let mock_ctx = ExecutorContext::new_mock(None).await;
13451 let version = Version(0);
13452
13453 frontend.hack_set_program(&ctx, program).await.unwrap();
13454 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13455 let sketch_id = sketch_object.id;
13456 let sketch = expect_sketch(sketch_object);
13457 let point_ids = vec![
13458 sketch.segments.first().unwrap().to_owned(),
13459 sketch.segments.get(1).unwrap().to_owned(),
13460 ];
13461
13462 let constraint = Constraint::Vertical(Vertical::Points {
13463 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13464 });
13465 let (src_delta, scene_delta) = frontend
13466 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13467 .await
13468 .unwrap();
13469 insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13470 assert_eq!(
13471 scene_delta.new_graph.objects.len(),
13472 5,
13473 "{:#?}",
13474 scene_delta.new_graph.objects
13475 );
13476
13477 ctx.close().await;
13478 mock_ctx.close().await;
13479 }
13480
13481 #[tokio::test(flavor = "multi_thread")]
13482 async fn test_points_horizontal() {
13483 let initial_source = "\
13484sketch001 = sketch(on = XY) {
13485 p0 = point(at = [var -2.23mm, var 3.1mm])
13486 pf = point(at = [4, 4])
13487}
13488";
13489
13490 let program = Program::parse(initial_source).unwrap().0.unwrap();
13491
13492 let mut frontend = FrontendState::new();
13493
13494 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13495 let mock_ctx = ExecutorContext::new_mock(None).await;
13496 let version = Version(0);
13497
13498 frontend.hack_set_program(&ctx, program).await.unwrap();
13499 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13500 let sketch_id = sketch_object.id;
13501 let sketch = expect_sketch(sketch_object);
13502 let point_ids = vec![
13503 sketch.segments.first().unwrap().to_owned(),
13504 sketch.segments.get(1).unwrap().to_owned(),
13505 ];
13506
13507 let constraint = Constraint::Horizontal(Horizontal::Points {
13508 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13509 });
13510 let (src_delta, scene_delta) = frontend
13511 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13512 .await
13513 .unwrap();
13514 insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13515 assert_eq!(
13516 scene_delta.new_graph.objects.len(),
13517 5,
13518 "{:#?}",
13519 scene_delta.new_graph.objects
13520 );
13521
13522 ctx.close().await;
13523 mock_ctx.close().await;
13524 }
13525
13526 #[tokio::test(flavor = "multi_thread")]
13527 async fn test_point_horizontal_with_origin() {
13528 let initial_source = "\
13529sketch001 = sketch(on = XY) {
13530 p0 = point(at = [var -2.23mm, var 3.1mm])
13531}
13532";
13533
13534 let program = Program::parse(initial_source).unwrap().0.unwrap();
13535
13536 let mut frontend = FrontendState::new();
13537
13538 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13539 let mock_ctx = ExecutorContext::new_mock(None).await;
13540 let version = Version(0);
13541
13542 frontend.hack_set_program(&ctx, program).await.unwrap();
13543 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13544 let sketch_id = sketch_object.id;
13545 let sketch = expect_sketch(sketch_object);
13546 let point_id = *sketch.segments.first().unwrap();
13547
13548 let constraint = Constraint::Horizontal(Horizontal::Points {
13549 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13550 });
13551 let (src_delta, scene_delta) = frontend
13552 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13553 .await
13554 .unwrap();
13555 insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13556 assert_eq!(
13557 scene_delta.new_graph.objects.len(),
13558 4,
13559 "{:#?}",
13560 scene_delta.new_graph.objects
13561 );
13562
13563 ctx.close().await;
13564 mock_ctx.close().await;
13565 }
13566
13567 #[tokio::test(flavor = "multi_thread")]
13568 async fn test_lines_equal_length() {
13569 let initial_source = "\
13570sketch(on = XY) {
13571 line(start = [var 1, var 2], end = [var 3, var 4])
13572 line(start = [var 5, var 6], end = [var 7, var 8])
13573}
13574";
13575
13576 let program = Program::parse(initial_source).unwrap().0.unwrap();
13577
13578 let mut frontend = FrontendState::new();
13579
13580 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13581 let mock_ctx = ExecutorContext::new_mock(None).await;
13582 let version = Version(0);
13583
13584 frontend.hack_set_program(&ctx, program).await.unwrap();
13585 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13586 let sketch_id = sketch_object.id;
13587 let sketch = expect_sketch(sketch_object);
13588 let line1_id = *sketch.segments.get(2).unwrap();
13589 let line2_id = *sketch.segments.get(5).unwrap();
13590
13591 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13592 lines: vec![line1_id, line2_id],
13593 });
13594 let (src_delta, scene_delta) = frontend
13595 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13596 .await
13597 .unwrap();
13598 insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13599 assert_eq!(
13600 scene_delta.new_graph.objects.len(),
13601 9,
13602 "{:#?}",
13603 scene_delta.new_graph.objects
13604 );
13605
13606 ctx.close().await;
13607 mock_ctx.close().await;
13608 }
13609
13610 #[tokio::test(flavor = "multi_thread")]
13611 async fn test_add_constraint_multi_line_equal_length() {
13612 let initial_source = "\
13613sketch(on = XY) {
13614 line(start = [var 1, var 2], end = [var 3, var 4])
13615 line(start = [var 5, var 6], end = [var 7, var 8])
13616 line(start = [var 9, var 10], end = [var 11, var 12])
13617}
13618";
13619
13620 let program = Program::parse(initial_source).unwrap().0.unwrap();
13621
13622 let mut frontend = FrontendState::new();
13623 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13624 let mock_ctx = ExecutorContext::new_mock(None).await;
13625 let version = Version(0);
13626
13627 frontend.hack_set_program(&ctx, program).await.unwrap();
13628 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13629 let sketch_id = sketch_object.id;
13630 let sketch = expect_sketch(sketch_object);
13631 let line1_id = *sketch.segments.get(2).unwrap();
13632 let line2_id = *sketch.segments.get(5).unwrap();
13633 let line3_id = *sketch.segments.get(8).unwrap();
13634
13635 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13636 lines: vec![line1_id, line2_id, line3_id],
13637 });
13638 let (src_delta, scene_delta) = frontend
13639 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13640 .await
13641 .unwrap();
13642 insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13643 let constraints = scene_delta
13644 .new_graph
13645 .objects
13646 .iter()
13647 .filter_map(|obj| {
13648 let ObjectKind::Constraint { constraint } = &obj.kind else {
13649 return None;
13650 };
13651 Some(constraint)
13652 })
13653 .collect::<Vec<_>>();
13654
13655 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13656 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13657 panic!("expected equal length constraint, got {:?}", constraints[0]);
13658 };
13659 assert_eq!(lines_equal_length.lines.len(), 3);
13660
13661 ctx.close().await;
13662 mock_ctx.close().await;
13663 }
13664
13665 #[tokio::test(flavor = "multi_thread")]
13666 async fn test_lines_parallel() {
13667 let initial_source = "\
13668sketch(on = XY) {
13669 line(start = [var 1, var 2], end = [var 3, var 4])
13670 line(start = [var 5, var 6], end = [var 7, var 8])
13671}
13672";
13673
13674 let program = Program::parse(initial_source).unwrap().0.unwrap();
13675
13676 let mut frontend = FrontendState::new();
13677
13678 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13679 let mock_ctx = ExecutorContext::new_mock(None).await;
13680 let version = Version(0);
13681
13682 frontend.hack_set_program(&ctx, program).await.unwrap();
13683 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13684 let sketch_id = sketch_object.id;
13685 let sketch = expect_sketch(sketch_object);
13686 let line1_id = *sketch.segments.get(2).unwrap();
13687 let line2_id = *sketch.segments.get(5).unwrap();
13688
13689 let constraint = Constraint::Parallel(Parallel {
13690 lines: vec![line1_id, line2_id],
13691 });
13692 let (src_delta, scene_delta) = frontend
13693 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13694 .await
13695 .unwrap();
13696 insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13697 assert_eq!(
13698 scene_delta.new_graph.objects.len(),
13699 9,
13700 "{:#?}",
13701 scene_delta.new_graph.objects
13702 );
13703
13704 ctx.close().await;
13705 mock_ctx.close().await;
13706 }
13707
13708 #[tokio::test(flavor = "multi_thread")]
13709 async fn test_lines_parallel_multiline() {
13710 let initial_source = "\
13711sketch(on = XY) {
13712 line(start = [var 1, var 2], end = [var 3, var 4])
13713 line(start = [var 5, var 6], end = [var 7, var 8])
13714 line(start = [var 9, var 10], end = [var 11, var 12])
13715}
13716";
13717
13718 let program = Program::parse(initial_source).unwrap().0.unwrap();
13719
13720 let mut frontend = FrontendState::new();
13721
13722 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13723 let mock_ctx = ExecutorContext::new_mock(None).await;
13724 let version = Version(0);
13725
13726 frontend.hack_set_program(&ctx, program).await.unwrap();
13727 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13728 let sketch_id = sketch_object.id;
13729 let sketch = expect_sketch(sketch_object);
13730 let line1_id = *sketch.segments.get(2).unwrap();
13731 let line2_id = *sketch.segments.get(5).unwrap();
13732 let line3_id = *sketch.segments.get(8).unwrap();
13733
13734 let constraint = Constraint::Parallel(Parallel {
13735 lines: vec![line1_id, line2_id, line3_id],
13736 });
13737 let (src_delta, scene_delta) = frontend
13738 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13739 .await
13740 .unwrap();
13741 insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13742
13743 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13744 let sketch = expect_sketch(sketch_object);
13745 assert_eq!(sketch.constraints.len(), 1);
13746
13747 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13748 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13749 panic!("Expected constraint object");
13750 };
13751 let Constraint::Parallel(parallel) = constraint else {
13752 panic!("Expected parallel constraint");
13753 };
13754 assert_eq!(parallel.lines.len(), 3);
13755
13756 ctx.close().await;
13757 mock_ctx.close().await;
13758 }
13759
13760 #[tokio::test(flavor = "multi_thread")]
13761 async fn test_lines_perpendicular() {
13762 let initial_source = "\
13763sketch(on = XY) {
13764 line(start = [var 1, var 2], end = [var 3, var 4])
13765 line(start = [var 5, var 6], end = [var 7, var 8])
13766}
13767";
13768
13769 let program = Program::parse(initial_source).unwrap().0.unwrap();
13770
13771 let mut frontend = FrontendState::new();
13772
13773 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13774 let mock_ctx = ExecutorContext::new_mock(None).await;
13775 let version = Version(0);
13776
13777 frontend.hack_set_program(&ctx, program).await.unwrap();
13778 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13779 let sketch_id = sketch_object.id;
13780 let sketch = expect_sketch(sketch_object);
13781 let line1_id = *sketch.segments.get(2).unwrap();
13782 let line2_id = *sketch.segments.get(5).unwrap();
13783
13784 let constraint = Constraint::Perpendicular(Perpendicular {
13785 lines: vec![line1_id, line2_id],
13786 });
13787 let (src_delta, scene_delta) = frontend
13788 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13789 .await
13790 .unwrap();
13791 insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13792 assert_eq!(
13793 scene_delta.new_graph.objects.len(),
13794 9,
13795 "{:#?}",
13796 scene_delta.new_graph.objects
13797 );
13798
13799 ctx.close().await;
13800 mock_ctx.close().await;
13801 }
13802
13803 #[tokio::test(flavor = "multi_thread")]
13804 async fn test_lines_angle() {
13805 let initial_source = "\
13806sketch(on = XY) {
13807 line(start = [var 1, var 2], end = [var 3, var 4])
13808 line(start = [var 5, var 6], end = [var 7, var 8])
13809}
13810";
13811
13812 let program = Program::parse(initial_source).unwrap().0.unwrap();
13813
13814 let mut frontend = FrontendState::new();
13815
13816 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13817 let mock_ctx = ExecutorContext::new_mock(None).await;
13818 let version = Version(0);
13819
13820 frontend.hack_set_program(&ctx, program).await.unwrap();
13821 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13822 let sketch_id = sketch_object.id;
13823 let sketch = expect_sketch(sketch_object);
13824 let line1_id = *sketch.segments.get(2).unwrap();
13825 let line2_id = *sketch.segments.get(5).unwrap();
13826
13827 let constraint = Constraint::Angle(Angle {
13828 lines: vec![line1_id, line2_id],
13829 angle: Number {
13830 value: 30.0,
13831 units: NumericSuffix::Deg,
13832 },
13833 sector: None,
13834 inverse: None,
13835 label_position: None,
13836 source: Default::default(),
13837 });
13838 let (src_delta, scene_delta) = frontend
13839 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13840 .await
13841 .unwrap();
13842 insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13843 assert_eq!(
13844 scene_delta.new_graph.objects.len(),
13845 9,
13846 "{:#?}",
13847 scene_delta.new_graph.objects
13848 );
13849
13850 ctx.close().await;
13851 mock_ctx.close().await;
13852 }
13853
13854 #[tokio::test(flavor = "multi_thread")]
13855 async fn test_lines_angle_with_sector_uses_angle_dimension() {
13856 let initial_source = "\
13857sketch(on = XY) {
13858 line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13859 line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13860}
13861";
13862
13863 let program = Program::parse(initial_source).unwrap().0.unwrap();
13864
13865 let mut frontend = FrontendState::new();
13866
13867 let mock_ctx = ExecutorContext::new_mock(None).await;
13868 let version = Version(0);
13869
13870 frontend.program = program.clone();
13871 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13872 frontend.update_state_after_exec(outcome, true);
13873 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13874 let sketch_id = sketch_object.id;
13875 let sketch = expect_sketch(sketch_object);
13876 let line1_id = *sketch.segments.get(2).unwrap();
13877 let line2_id = *sketch.segments.get(5).unwrap();
13878
13879 let constraint = Constraint::Angle(Angle {
13880 lines: vec![line1_id, line2_id],
13881 angle: Number {
13882 value: 270.0,
13883 units: NumericSuffix::Deg,
13884 },
13885 sector: Some(1),
13886 inverse: Some(true),
13887 label_position: Some(Point2d {
13888 x: Number {
13889 value: -0.73,
13890 units: NumericSuffix::Mm,
13891 },
13892 y: Number {
13893 value: 0.75,
13894 units: NumericSuffix::Mm,
13895 },
13896 }),
13897 source: Default::default(),
13898 });
13899 let (src_delta, _) = frontend
13900 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13901 .await
13902 .unwrap();
13903 assert_eq!(
13904 src_delta.text.as_str(),
13905 "\
13906sketch(on = XY) {
13907 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13908 line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13909 angleDimension(
13910 lines = [line1, line2],
13911 sector = 1,
13912 inverse = true,
13913 labelPosition = [-0.73mm, 0.75mm],
13914) == 270deg
13915}
13916"
13917 );
13918
13919 mock_ctx.close().await;
13920 }
13921
13922 #[tokio::test(flavor = "multi_thread")]
13923 async fn test_segments_tangent() {
13924 let initial_source = "\
13925sketch(on = XY) {
13926 line(start = [var 1, var 2], end = [var 3, var 4])
13927 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13928}
13929";
13930
13931 let program = Program::parse(initial_source).unwrap().0.unwrap();
13932
13933 let mut frontend = FrontendState::new();
13934
13935 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13936 let mock_ctx = ExecutorContext::new_mock(None).await;
13937 let version = Version(0);
13938
13939 frontend.hack_set_program(&ctx, program).await.unwrap();
13940 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13941 let sketch_id = sketch_object.id;
13942 let sketch = expect_sketch(sketch_object);
13943 let line1_id = *sketch.segments.get(2).unwrap();
13944 let arc1_id = *sketch.segments.get(6).unwrap();
13945
13946 let constraint = Constraint::Tangent(Tangent {
13947 input: vec![line1_id, arc1_id],
13948 });
13949 let (src_delta, scene_delta) = frontend
13950 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13951 .await
13952 .unwrap();
13953 insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
13954 assert_eq!(
13955 scene_delta.new_graph.objects.len(),
13956 10,
13957 "{:#?}",
13958 scene_delta.new_graph.objects
13959 );
13960
13961 ctx.close().await;
13962 mock_ctx.close().await;
13963 }
13964
13965 #[tokio::test(flavor = "multi_thread")]
13966 async fn test_point_midpoint() {
13967 let initial_source = "\
13968sketch(on = XY) {
13969 point(at = [var 1, var 1])
13970 line(start = [var 0, var 0], end = [var 6, var 4])
13971}
13972";
13973
13974 let program = Program::parse(initial_source).unwrap().0.unwrap();
13975
13976 let mut frontend = FrontendState::new();
13977
13978 let ctx = ExecutorContext::new_mock(None).await;
13979 let version = Version(0);
13980
13981 frontend.program = program.clone();
13982 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13983 frontend.update_state_after_exec(outcome, true);
13984 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13985 let sketch_id = sketch_object.id;
13986 let sketch = expect_sketch(sketch_object);
13987 let point_id = *sketch.segments.first().unwrap();
13988 let line_id = *sketch.segments.get(3).unwrap();
13989
13990 let constraint = Constraint::Midpoint(Midpoint {
13991 point: ConstraintSegment::from(point_id),
13992 segment: line_id,
13993 });
13994 let (src_delta, scene_delta) = frontend
13995 .add_constraint(&ctx, version, sketch_id, constraint)
13996 .await
13997 .unwrap();
13998 insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
13999 assert_eq!(
14000 scene_delta.new_graph.objects.len(),
14001 7,
14002 "{:#?}",
14003 scene_delta.new_graph.objects
14004 );
14005
14006 ctx.close().await;
14007 }
14008
14009 #[tokio::test(flavor = "multi_thread")]
14010 async fn test_segments_symmetric() {
14011 let initial_source = "\
14012sketch(on = XY) {
14013 line(start = [var 0, var 0], end = [var 0, var 4])
14014 line(start = [var 4, var 0], end = [var 4, var 4])
14015 line(start = [var 2, var -1], end = [var 2, var 5])
14016}
14017";
14018
14019 let program = Program::parse(initial_source).unwrap().0.unwrap();
14020
14021 let mut frontend = FrontendState::new();
14022
14023 let ctx = ExecutorContext::new_mock(None).await;
14024 let version = Version(0);
14025
14026 frontend.program = program.clone();
14027 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14028 frontend.update_state_after_exec(outcome, true);
14029 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14030 let sketch_id = sketch_object.id;
14031 let sketch = expect_sketch(sketch_object);
14032 let line1_id = *sketch.segments.get(2).unwrap();
14033 let line2_id = *sketch.segments.get(5).unwrap();
14034 let axis_id = *sketch.segments.get(8).unwrap();
14035
14036 let constraint = Constraint::Symmetric(Symmetric {
14037 input: vec![line1_id, line2_id],
14038 axis: axis_id,
14039 });
14040 let (src_delta, scene_delta) = frontend
14041 .add_constraint(&ctx, version, sketch_id, constraint)
14042 .await
14043 .unwrap();
14044 insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14045 assert_eq!(
14046 scene_delta.new_graph.objects.len(),
14047 12,
14048 "{:#?}",
14049 scene_delta.new_graph.objects
14050 );
14051
14052 ctx.close().await;
14053 }
14054
14055 #[tokio::test(flavor = "multi_thread")]
14056 async fn test_point_arc_midpoint() {
14057 let initial_source = "\
14058sketch(on = XY) {
14059 point(at = [var 6, var 3])
14060 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14061}
14062";
14063
14064 let program = Program::parse(initial_source).unwrap().0.unwrap();
14065
14066 let mut frontend = FrontendState::new();
14067
14068 let ctx = ExecutorContext::new_mock(None).await;
14069 let version = Version(0);
14070
14071 frontend.program = program.clone();
14072 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14073 frontend.update_state_after_exec(outcome, true);
14074 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14075 let sketch_id = sketch_object.id;
14076 let sketch = expect_sketch(sketch_object);
14077 let point_id = *sketch.segments.first().unwrap();
14078 let arc_id = *sketch.segments.get(4).unwrap();
14079
14080 let constraint = Constraint::Midpoint(Midpoint {
14081 point: ConstraintSegment::from(point_id),
14082 segment: arc_id,
14083 });
14084 let (src_delta, scene_delta) = frontend
14085 .add_constraint(&ctx, version, sketch_id, constraint)
14086 .await
14087 .unwrap();
14088 insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14089 assert_eq!(
14090 scene_delta.new_graph.objects.len(),
14091 8,
14092 "{:#?}",
14093 scene_delta.new_graph.objects
14094 );
14095
14096 ctx.close().await;
14097 }
14098
14099 #[tokio::test(flavor = "multi_thread")]
14100 async fn test_origin_line_midpoint() {
14101 let initial_source = "\
14102sketch(on = XY) {
14103 line(start = [var 0, var 0], end = [var 6, var 4])
14104}
14105";
14106
14107 let program = Program::parse(initial_source).unwrap().0.unwrap();
14108
14109 let mut frontend = FrontendState::new();
14110
14111 let ctx = ExecutorContext::new_mock(None).await;
14112 let version = Version(0);
14113
14114 frontend.program = program.clone();
14115 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14116 frontend.update_state_after_exec(outcome, true);
14117 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14118 let sketch_id = sketch_object.id;
14119 let sketch = expect_sketch(sketch_object);
14120 let line_id = *sketch.segments.get(2).unwrap();
14121
14122 let constraint = Constraint::Midpoint(Midpoint {
14123 point: ConstraintSegment::ORIGIN,
14124 segment: line_id,
14125 });
14126 let (src_delta, scene_delta) = frontend
14127 .add_constraint(&ctx, version, sketch_id, constraint)
14128 .await
14129 .unwrap();
14130 insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14131 assert_eq!(
14132 scene_delta.new_graph.objects.len(),
14133 6,
14134 "{:#?}",
14135 scene_delta.new_graph.objects
14136 );
14137
14138 ctx.close().await;
14139 }
14140
14141 #[tokio::test(flavor = "multi_thread")]
14142 async fn test_origin_arc_midpoint() {
14143 let initial_source = "\
14144sketch(on = XY) {
14145 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14146}
14147";
14148
14149 let program = Program::parse(initial_source).unwrap().0.unwrap();
14150
14151 let mut frontend = FrontendState::new();
14152
14153 let ctx = ExecutorContext::new_mock(None).await;
14154 let version = Version(0);
14155
14156 frontend.program = program.clone();
14157 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14158 frontend.update_state_after_exec(outcome, true);
14159 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14160 let sketch_id = sketch_object.id;
14161 let sketch = expect_sketch(sketch_object);
14162 let arc_id = *sketch.segments.get(3).unwrap();
14163
14164 let constraint = Constraint::Midpoint(Midpoint {
14165 point: ConstraintSegment::ORIGIN,
14166 segment: arc_id,
14167 });
14168 let (src_delta, scene_delta) = frontend
14169 .add_constraint(&ctx, version, sketch_id, constraint)
14170 .await
14171 .unwrap();
14172 insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14173 assert_eq!(
14174 scene_delta.new_graph.objects.len(),
14175 7,
14176 "{:#?}",
14177 scene_delta.new_graph.objects
14178 );
14179
14180 ctx.close().await;
14181 }
14182
14183 #[tokio::test(flavor = "multi_thread")]
14184 async fn test_segments_symmetric_arcs() {
14185 let initial_source = "\
14186sketch(on = XY) {
14187 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14188 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14189 line(start = [var 0, var -10], end = [var 0, var 10])
14190}
14191";
14192
14193 let program = Program::parse(initial_source).unwrap().0.unwrap();
14194
14195 let mut frontend = FrontendState::new();
14196
14197 let ctx = ExecutorContext::new_mock(None).await;
14198 let version = Version(0);
14199
14200 frontend.program = program.clone();
14201 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14202 frontend.update_state_after_exec(outcome, true);
14203 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14204 let sketch_id = sketch_object.id;
14205 let sketch = expect_sketch(sketch_object);
14206 let arc1_id = *sketch.segments.get(3).unwrap();
14207 let arc2_id = *sketch.segments.get(7).unwrap();
14208 let axis_id = *sketch.segments.get(10).unwrap();
14209
14210 let constraint = Constraint::Symmetric(Symmetric {
14211 input: vec![arc1_id, arc2_id],
14212 axis: axis_id,
14213 });
14214 let (src_delta, scene_delta) = frontend
14215 .add_constraint(&ctx, version, sketch_id, constraint)
14216 .await
14217 .unwrap();
14218 insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14219 assert_eq!(
14220 scene_delta.new_graph.objects.len(),
14221 14,
14222 "{:#?}",
14223 scene_delta.new_graph.objects
14224 );
14225
14226 ctx.close().await;
14227 }
14228
14229 #[tokio::test(flavor = "multi_thread")]
14230 async fn test_sketch_on_face_simple() {
14231 let initial_source = "\
14232len = 2mm
14233cube = startSketchOn(XY)
14234 |> startProfile(at = [0, 0])
14235 |> line(end = [len, 0], tag = $side)
14236 |> line(end = [0, len])
14237 |> line(end = [-len, 0])
14238 |> line(end = [0, -len])
14239 |> close()
14240 |> extrude(length = len)
14241
14242face = faceOf(cube, face = side)
14243";
14244
14245 let program = Program::parse(initial_source).unwrap().0.unwrap();
14246
14247 let mut frontend = FrontendState::new();
14248
14249 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14250 let mock_ctx = ExecutorContext::new_mock(None).await;
14251 let version = Version(0);
14252
14253 frontend.hack_set_program(&ctx, program).await.unwrap();
14254 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14255 let face_id = face_object.id;
14256
14257 let sketch_args = SketchCtor {
14258 on: Plane::Object(face_id),
14259 };
14260 let (_src_delta, scene_delta, sketch_id) = frontend
14261 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14262 .await
14263 .unwrap();
14264 assert_eq!(sketch_id, ObjectId(2));
14265 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14266 let sketch_object = &scene_delta.new_graph.objects[2];
14267 assert_eq!(sketch_object.id, ObjectId(2));
14268 assert_eq!(
14269 sketch_object.kind,
14270 ObjectKind::Sketch(Sketch {
14271 args: SketchCtor {
14272 on: Plane::Object(face_id),
14273 },
14274 plane: face_id,
14275 segments: vec![],
14276 constraints: vec![],
14277 })
14278 );
14279 assert_eq!(scene_delta.new_graph.objects.len(), 8);
14280
14281 ctx.close().await;
14282 mock_ctx.close().await;
14283 }
14284
14285 #[tokio::test(flavor = "multi_thread")]
14286 async fn test_new_sketch_on_primitive_index_face() {
14287 let initial_source = "\
14288@settings(kclVersion = 2.0)
14289
14290sketch001 = sketch(on = XY) {
14291 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14292}
14293extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14294shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14295 let program = Program::parse(initial_source).unwrap().0.unwrap();
14296 let ctx = ExecutorContext::new_mock(None).await;
14297 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14298 let solid_id = match outcome.variables.get("shell001") {
14299 Some(KclValueView::Solid { value }) => value.id,
14300 value => panic!("expected shell001 to be a solid, got {value:?}"),
14301 };
14302 let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14303
14304 let mut ast = program.ast;
14305 let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14306 let face_expr = sketch_on_ast_expr(
14307 &mut ast,
14308 &scene_graph,
14309 &solid_references,
14310 &Plane::PrimitiveFace(crate::frontend::api::PrimitiveFacePlane { solid_id, index: 6 }),
14311 )
14312 .unwrap();
14313 let face_decl = ast::VariableDeclaration::new(
14314 ast::VariableDeclarator::new("face001", face_expr),
14315 ast::ItemVisibility::Default,
14316 ast::VariableKind::Const,
14317 );
14318 ast.body
14319 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14320 face_decl,
14321 ))));
14322 let face_source = source_from_ast(&ast);
14323 let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14324 insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14325
14326 let program = Program::parse(&new_source).unwrap().0.unwrap();
14327 ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14328 ctx.close().await;
14329 }
14330
14331 #[tokio::test(flavor = "multi_thread")]
14332 async fn test_sketch_on_wall_artifact_from_region_extrude() {
14333 let initial_source = "\
14334s = sketch(on = YZ) {
14335 line1 = line(start = [0, 0], end = [0, 1])
14336 line2 = line(start = [0, 1], end = [1, 1])
14337 line3 = line(start = [1, 1], end = [0, 0])
14338}
14339region001 = region(point = [0.1, 0.1], sketch = s)
14340extrude001 = extrude(region001, length = 5)
14341";
14342
14343 let program = Program::parse(initial_source).unwrap().0.unwrap();
14344
14345 let mut frontend = FrontendState::new();
14346 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14347 let version = Version(0);
14348
14349 frontend.hack_set_program(&ctx, program).await.unwrap();
14350 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14351
14352 let sketch_args = SketchCtor {
14353 on: Plane::Object(wall_object_id),
14354 };
14355 let (src_delta, _scene_delta, _sketch_id) = frontend
14356 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14357 .await
14358 .unwrap();
14359 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14360
14361 ctx.close().await;
14362 }
14363
14364 #[tokio::test(flavor = "multi_thread")]
14365 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14366 let initial_source = "\
14367sketch001 = sketch(on = YZ) {
14368 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14369 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14370 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14371 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14372 coincident([line1.end, line2.start])
14373 coincident([line2.end, line3.start])
14374 coincident([line3.end, line4.start])
14375 coincident([line4.end, line1.start])
14376 parallel([line2, line4])
14377 parallel([line3, line1])
14378 perpendicular([line1, line2])
14379 horizontal(line3)
14380 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14381}
14382region001 = region(point = [3.1, 3.74], sketch = sketch001)
14383extrude001 = extrude(region001, length = 5)
14384";
14385
14386 let program = Program::parse(initial_source).unwrap().0.unwrap();
14387
14388 let mut frontend = FrontendState::new();
14389 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14390 let version = Version(0);
14391
14392 frontend.hack_set_program(&ctx, program).await.unwrap();
14393 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14394
14395 let sketch_args = SketchCtor {
14396 on: Plane::Object(wall_object_id),
14397 };
14398 let (src_delta, _scene_delta, _sketch_id) = frontend
14399 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14400 .await
14401 .unwrap();
14402 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14403
14404 ctx.close().await;
14405 }
14406
14407 #[tokio::test(flavor = "multi_thread")]
14408 async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14409 let initial_source = "\
14410@settings(kclVersion = 2.0)
14411
14412sketch001 = sketch(on = XY) {
14413 circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14414 circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14415}
14416hidden001 = hide(sketch001)
14417region001 = region(segments = [sketch001.circle2])
14418region002 = region(segments = [sketch001.circle1])
14419extrude001 = extrude([region001, region002], length = 5)
14420";
14421
14422 let program = Program::parse(initial_source).unwrap().0.unwrap();
14423 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14424 let version = Version(0);
14425
14426 for (solid_output_index, expected_face) in [
14427 (0, "faceOf(extrude001[0], face = END)"),
14428 (1, "faceOf(extrude001[1], face = END)"),
14429 ] {
14430 let mut frontend = FrontendState::new();
14431 frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14432 let cap_object_id = find_cap_object_id_with_solid_output_index(
14433 &frontend.scene_graph,
14434 crate::frontend::api::CapKind::End,
14435 solid_output_index,
14436 )
14437 .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14438
14439 let sketch_args = SketchCtor {
14440 on: Plane::Object(cap_object_id),
14441 };
14442 let (src_delta, _scene_delta, _sketch_id) = frontend
14443 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14444 .await
14445 .unwrap();
14446
14447 assert!(
14448 src_delta.text.contains(expected_face),
14449 "expected `{expected_face}` in:\n{}",
14450 src_delta.text
14451 );
14452 assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14453 }
14454
14455 ctx.close().await;
14456 }
14457
14458 #[tokio::test(flavor = "multi_thread")]
14459 async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14460 let initial_source = "\
14461@settings(kclVersion = 2.0)
14462
14463sketch001 = sketch(on = XY) {
14464 rect1Line1 = line(start = [0, 0], end = [1, 0])
14465 rect1Line2 = line(start = [1, 0], end = [1, 1])
14466 rect1Line3 = line(start = [1, 1], end = [0, 1])
14467 rect1Line4 = line(start = [0, 1], end = [0, 0])
14468 rect2Line1 = line(start = [3, 0], end = [4, 0])
14469 rect2Line2 = line(start = [4, 0], end = [4, 1])
14470 rect2Line3 = line(start = [4, 1], end = [3, 1])
14471 rect2Line4 = line(start = [3, 1], end = [3, 0])
14472}
14473hidden001 = hide(sketch001)
14474region001 = region(segments = [
14475 sketch001.rect1Line4,
14476 sketch001.rect1Line1
14477])
14478region002 = region(segments = [
14479 sketch001.rect2Line4,
14480 sketch001.rect2Line1
14481])
14482extrude001 = extrude([region001, region002], length = 5)
14483";
14484
14485 let program = Program::parse(initial_source).unwrap().0.unwrap();
14486 let mut frontend = FrontendState::new();
14487 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14488 let version = Version(0);
14489
14490 frontend.hack_set_program(&ctx, program).await.unwrap();
14491 let region_call = "\
14492region(segments = [
14493 sketch001.rect1Line4,
14494 sketch001.rect1Line1
14495])";
14496 let region_call_start = initial_source.find(region_call).unwrap();
14497 let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14498 let segment_call = "line(start = [0, 0], end = [1, 0])";
14499 let segment_call_start = initial_source.find(segment_call).unwrap();
14500 let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14501 let wall_object_id = frontend
14502 .scene_graph
14503 .objects
14504 .iter()
14505 .find_map(|object| match &object.kind {
14506 ObjectKind::Wall(wall)
14507 if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14508 && wall.source.segment.range == segment_range =>
14509 {
14510 Some(object.id)
14511 }
14512 _ => None,
14513 })
14514 .expect("expected a wall object for region001.tags.rect1Line1");
14515
14516 let sketch_args = SketchCtor {
14517 on: Plane::Object(wall_object_id),
14518 };
14519 let (src_delta, _scene_delta, _sketch_id) = frontend
14520 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14521 .await
14522 .unwrap();
14523
14524 let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14525 assert!(
14526 src_delta.text.contains(expected_face),
14527 "expected `{expected_face}` in:\n{}",
14528 src_delta.text
14529 );
14530 assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14531
14532 ctx.close().await;
14533 }
14534
14535 #[test]
14536 fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14537 let source = "\
14538sketch001 = sketch(on = XY) {
14539 line(start = [0, 0], end = [1, 0])
14540}
14541part = subtract(boxSolid, tools = [cutSolid])
14542 |> appearance(color = \"#8f96a3\")
14543";
14544 let ast = Program::parse(source).unwrap().0.unwrap().ast;
14545 let line_start = source.find("line").unwrap();
14546 let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14547 let line_ref = SourceRef::Simple {
14548 range: [line_start, line_end, 0].into(),
14549 node_path: None,
14550 };
14551 assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14552
14553 let subtract_start = source.find("subtract").unwrap();
14554 let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14555 let subtract_ref = SourceRef::Simple {
14556 range: [subtract_start, subtract_end, 0].into(),
14557 node_path: None,
14558 };
14559 assert_eq!(
14560 variable_name_containing_source_ref(&ast, &subtract_ref),
14561 Some("part".to_owned())
14562 );
14563 }
14564
14565 #[tokio::test(flavor = "multi_thread")]
14566 async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14567 clear_mem_cache().await;
14568 let source = "\
14569boxSolid = startSketchOn(XY)
14570 |> startProfile(at = [0, 0])
14571 |> line(end = [4, 0], tag = $bottomEdge)
14572 |> line(end = [0, 4])
14573 |> line(end = [-4, 0])
14574 |> close()
14575 |> extrude(length = 10)
14576cutSolid = startSketchOn(XY)
14577 |> startProfile(at = [1, 1])
14578 |> line(end = [1, 0])
14579 |> line(end = [0, 1])
14580 |> line(end = [-1, 0])
14581 |> close()
14582 |> extrude(length = 10)
14583part = subtract(boxSolid, tools = [cutSolid])
14584 |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14585";
14586 let program = Program::parse(source).unwrap().0.unwrap();
14587 let mut frontend = FrontendState::new();
14588 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14589 match frontend.hack_set_program(&ctx, program).await.unwrap() {
14590 SetProgramOutcome::Success { .. } => {}
14591 SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14592 }
14593
14594 let sweep_call_start = source.find("extrude").unwrap();
14595 let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14596 let part_call_start = source.find("subtract").unwrap();
14597 let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14598 let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14599 let composite_range = [part_call_start, part_call_end, 0].into();
14600
14601 let cap_object = frontend
14602 .scene_graph
14603 .objects
14604 .iter()
14605 .find(|object| {
14606 matches!(
14607 &object.kind,
14608 ObjectKind::Cap(crate::frontend::api::Cap {
14609 kind: crate::frontend::api::CapKind::End,
14610 source,
14611 ..
14612 }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14613 )
14614 })
14615 .expect("expected end cap object to trace through subtract and original extrude");
14616
14617 let mut ast = frontend.program.ast.clone();
14618 let cap_expr = sketch_on_ast_expr(
14619 &mut ast,
14620 &frontend.scene_graph,
14621 &frontend.solid_references,
14622 &Plane::Object(cap_object.id),
14623 )
14624 .unwrap();
14625 let cap_face_decl = ast::VariableDeclaration::new(
14626 ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14627 ast::ItemVisibility::Default,
14628 ast::VariableKind::Const,
14629 );
14630 ast.body
14631 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14632 cap_face_decl,
14633 ))));
14634 let generated_source = source_from_ast(&ast);
14635
14636 assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14637 assert!(!generated_source.contains("faceOf(boxSolid"));
14638 let ast::Expr::CallExpressionKw(call) = cap_expr else {
14639 panic!("expected faceOf call");
14640 };
14641 assert_eq!(call.callee.name.name, "faceOf");
14642 let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14643 panic!("expected solid name");
14644 };
14645 assert_eq!(solid_name.name.name, "part");
14646 let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14647 panic!("expected face name");
14648 };
14649 assert_eq!(face_name.name.name, "END");
14650
14651 ctx.close().await;
14652 }
14653
14654 #[tokio::test(flavor = "multi_thread")]
14655 async fn test_sketch_on_plane_incremental() {
14656 let initial_source = "\
14657len = 2mm
14658cube = startSketchOn(XY)
14659 |> startProfile(at = [0, 0])
14660 |> line(end = [len, 0], tag = $side)
14661 |> line(end = [0, len])
14662 |> line(end = [-len, 0])
14663 |> line(end = [0, -len])
14664 |> close()
14665 |> extrude(length = len)
14666
14667plane = planeOf(cube, face = side)
14668";
14669
14670 let program = Program::parse(initial_source).unwrap().0.unwrap();
14671
14672 let mut frontend = FrontendState::new();
14673
14674 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14675 let mock_ctx = ExecutorContext::new_mock(None).await;
14676 let version = Version(0);
14677
14678 frontend.hack_set_program(&ctx, program).await.unwrap();
14679 let plane_object = frontend
14681 .scene_graph
14682 .objects
14683 .iter()
14684 .rev()
14685 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14686 .unwrap();
14687 let plane_id = plane_object.id;
14688
14689 let sketch_args = SketchCtor {
14690 on: Plane::Object(plane_id),
14691 };
14692 let (src_delta, scene_delta, sketch_id) = frontend
14693 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14694 .await
14695 .unwrap();
14696 insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14697 assert_eq!(sketch_id, ObjectId(2));
14698 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14699 let sketch_object = &scene_delta.new_graph.objects[2];
14700 assert_eq!(sketch_object.id, ObjectId(2));
14701 assert_eq!(
14702 sketch_object.kind,
14703 ObjectKind::Sketch(Sketch {
14704 args: SketchCtor {
14705 on: Plane::Object(plane_id),
14706 },
14707 plane: plane_id,
14708 segments: vec![],
14709 constraints: vec![],
14710 })
14711 );
14712 assert_eq!(scene_delta.new_graph.objects.len(), 9);
14713
14714 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14715 assert_eq!(plane_object.id, plane_id);
14716 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14717
14718 ctx.close().await;
14719 mock_ctx.close().await;
14720 }
14721
14722 #[tokio::test(flavor = "multi_thread")]
14723 async fn test_new_sketch_uses_unique_variable_name() {
14724 let initial_source = "\
14725sketch1 = sketch(on = XY) {
14726}
14727";
14728
14729 let program = Program::parse(initial_source).unwrap().0.unwrap();
14730
14731 let mut frontend = FrontendState::new();
14732 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14733 let version = Version(0);
14734
14735 frontend.hack_set_program(&ctx, program).await.unwrap();
14736
14737 let sketch_args = SketchCtor {
14738 on: Plane::Default(PlaneName::Yz),
14739 };
14740 let (src_delta, _, _) = frontend
14741 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14742 .await
14743 .unwrap();
14744
14745 insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14746
14747 ctx.close().await;
14748 }
14749
14750 #[tokio::test(flavor = "multi_thread")]
14751 async fn test_new_sketch_twice_using_same_plane() {
14752 let initial_source = "\
14753sketch1 = sketch(on = XY) {
14754}
14755";
14756
14757 let program = Program::parse(initial_source).unwrap().0.unwrap();
14758
14759 let mut frontend = FrontendState::new();
14760 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14761 let version = Version(0);
14762
14763 frontend.hack_set_program(&ctx, program).await.unwrap();
14764
14765 let sketch_args = SketchCtor {
14766 on: Plane::Default(PlaneName::Xy),
14767 };
14768 let (src_delta, _, _) = frontend
14769 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14770 .await
14771 .unwrap();
14772
14773 insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14774
14775 ctx.close().await;
14776 }
14777
14778 #[tokio::test(flavor = "multi_thread")]
14779 async fn test_sketch_mode_reuses_cached_on_expression() {
14780 let initial_source = "\
14781width = 2mm
14782sketch(on = offsetPlane(XY, offset = width)) {
14783 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14784 distance([line1.start, line1.end]) == width
14785}
14786";
14787 let program = Program::parse(initial_source).unwrap().0.unwrap();
14788
14789 let mut frontend = FrontendState::new();
14790 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14791 let mock_ctx = ExecutorContext::new_mock(None).await;
14792 let version = Version(0);
14793 let project_id = ProjectId(0);
14794 let file_id = FileId(0);
14795
14796 frontend.hack_set_program(&ctx, program).await.unwrap();
14797 let initial_object_count = frontend.scene_graph.objects.len();
14798 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14799 .expect("Expected sketch object to exist")
14800 .id;
14801
14802 let scene_delta = frontend
14805 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14806 .await
14807 .unwrap();
14808 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14809
14810 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14813 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14814
14815 ctx.close().await;
14816 mock_ctx.close().await;
14817 }
14818
14819 #[tokio::test(flavor = "multi_thread")]
14820 async fn test_edit_sketch_nested_in_pipe() {
14821 clear_mem_cache().await;
14822 let source = r#"
14823profile = sketch(on = XY) {
14824 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14825}
14826 |> translate(x = 2mm)
14827"#;
14828 let program = Program::parse_no_errs(source).unwrap();
14829 let mut frontend = FrontendState::new();
14830 let mock_ctx = ExecutorContext::new_mock(None).await;
14831 let version = Version(0);
14832
14833 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14834 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14835 .expect("Expected piped sketch object")
14836 .id;
14837
14838 let scene_delta = frontend
14839 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14840 .await
14841 .unwrap();
14842 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14843 assert!(
14844 scene_delta
14845 .new_graph
14846 .objects
14847 .iter()
14848 .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14849 "Expected the piped sketch's segments to be present in sketch mode"
14850 );
14851
14852 clear_mem_cache().await;
14853 mock_ctx.close().await;
14854 }
14855
14856 #[tokio::test(flavor = "multi_thread")]
14857 async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14858 clear_mem_cache().await;
14859 let source = r#"
14860useFirstProfile = true
14861
14862profile = if useFirstProfile {
14863 sketch(on = XY) {
14864 line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14865 }
14866} else {
14867 sketch(on = XY) {
14868 line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14869 }
14870}
14871"#;
14872 let program = Program::parse_no_errs(source).unwrap();
14873 let mut frontend = FrontendState::new();
14874 let mock_ctx = ExecutorContext::new_mock(None).await;
14875 let version = Version(0);
14876
14877 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14878 let sketch_object =
14879 find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14880 let sketch_id = sketch_object.id;
14881 let sketch = expect_sketch(sketch_object);
14882 let line_end_id = *sketch
14883 .segments
14884 .get(1)
14885 .expect("Expected the active branch's line end point");
14886
14887 let scene_delta = frontend
14888 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14889 .await
14890 .unwrap();
14891 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14892
14893 let segments = vec![ExistingSegmentCtor {
14894 id: line_end_id,
14895 ctor: SegmentCtor::Point(PointCtor {
14896 position: Point2d {
14897 x: Expr::Var(Number {
14898 value: 30.0,
14899 units: NumericSuffix::Mm,
14900 }),
14901 y: Expr::Var(Number {
14902 value: 15.0,
14903 units: NumericSuffix::Mm,
14904 }),
14905 },
14906 }),
14907 }];
14908 let (source_delta, _) = frontend
14909 .edit_segments(&mock_ctx, version, sketch_id, segments)
14910 .await
14911 .unwrap();
14912 assert!(
14913 source_delta
14914 .text
14915 .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14916 "Expected the active branch's dragged variables to be updated:\n{}",
14917 source_delta.text
14918 );
14919 assert!(
14920 source_delta
14921 .text
14922 .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14923 "Expected the inactive branch to remain unchanged:\n{}",
14924 source_delta.text
14925 );
14926
14927 clear_mem_cache().await;
14928 mock_ctx.close().await;
14929 }
14930
14931 #[tokio::test(flavor = "multi_thread")]
14932 async fn test_multiple_sketch_blocks() {
14933 let initial_source = "\
14934// Cube that requires the engine.
14935width = 2
14936sketch001 = startSketchOn(XY)
14937profile001 = startProfile(sketch001, at = [0, 0])
14938 |> yLine(length = width, tag = $seg1)
14939 |> xLine(length = width)
14940 |> yLine(length = -width)
14941 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14942 |> close()
14943extrude001 = extrude(profile001, length = width)
14944
14945// Get a value that requires the engine.
14946x = segLen(seg1)
14947
14948// Triangle with side length 2*x.
14949sketch(on = XY) {
14950 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14951 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14952 coincident([line1.end, line2.start])
14953 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14954 coincident([line2.end, line3.start])
14955 coincident([line3.end, line1.start])
14956 equalLength([line3, line1])
14957 equalLength([line1, line2])
14958 distance([line1.start, line1.end]) == 2*x
14959}
14960
14961// Line segment with length x.
14962sketch2 = sketch(on = XY) {
14963 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14964 distance([line1.start, line1.end]) == x
14965}
14966";
14967
14968 let program = Program::parse(initial_source).unwrap().0.unwrap();
14969
14970 let mut frontend = FrontendState::new();
14971
14972 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14973 let mock_ctx = ExecutorContext::new_mock(None).await;
14974 let version = Version(0);
14975 let project_id = ProjectId(0);
14976 let file_id = FileId(0);
14977
14978 frontend.hack_set_program(&ctx, program).await.unwrap();
14979 let sketch_objects = frontend
14980 .scene_graph
14981 .objects
14982 .iter()
14983 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14984 .collect::<Vec<_>>();
14985 let sketch1_id = sketch_objects.first().unwrap().id;
14986 let sketch2_id = sketch_objects.get(1).unwrap().id;
14987 let point1_id = ObjectId(sketch1_id.0 + 1);
14989 let point2_id = ObjectId(sketch2_id.0 + 1);
14991
14992 let scene_delta = frontend
15001 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15002 .await
15003 .unwrap();
15004 assert_eq!(
15005 scene_delta.new_graph.objects.len(),
15006 18,
15007 "{:#?}",
15008 scene_delta.new_graph.objects
15009 );
15010
15011 let point_ctor = PointCtor {
15013 position: Point2d {
15014 x: Expr::Var(Number {
15015 value: 1.0,
15016 units: NumericSuffix::Mm,
15017 }),
15018 y: Expr::Var(Number {
15019 value: 2.0,
15020 units: NumericSuffix::Mm,
15021 }),
15022 },
15023 };
15024 let segments = vec![ExistingSegmentCtor {
15025 id: point1_id,
15026 ctor: SegmentCtor::Point(point_ctor),
15027 }];
15028 let (src_delta, _) = frontend
15029 .edit_segments(&mock_ctx, version, sketch1_id, segments)
15030 .await
15031 .unwrap();
15032 insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15034 let edited_sketch1_source = src_delta.text.clone();
15035
15036 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15038 assert_eq!(src_delta.text, edited_sketch1_source);
15039 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15047 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15048
15049 let scene_delta = frontend
15057 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15058 .await
15059 .unwrap();
15060 assert_eq!(
15061 scene_delta.new_graph.objects.len(),
15062 24,
15063 "{:#?}",
15064 scene_delta.new_graph.objects
15065 );
15066
15067 let point_ctor = PointCtor {
15069 position: Point2d {
15070 x: Expr::Var(Number {
15071 value: 3.0,
15072 units: NumericSuffix::Mm,
15073 }),
15074 y: Expr::Var(Number {
15075 value: 4.0,
15076 units: NumericSuffix::Mm,
15077 }),
15078 },
15079 };
15080 let segments = vec![ExistingSegmentCtor {
15081 id: point2_id,
15082 ctor: SegmentCtor::Point(point_ctor),
15083 }];
15084 let (src_delta, _) = frontend
15085 .edit_segments(&mock_ctx, version, sketch2_id, segments)
15086 .await
15087 .unwrap();
15088 insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15090 let edited_sketch2_source = src_delta.text.clone();
15091
15092 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15094 assert_eq!(src_delta.text, edited_sketch2_source);
15095
15096 ctx.close().await;
15097 mock_ctx.close().await;
15098 }
15099
15100 #[tokio::test(flavor = "multi_thread")]
15101 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15102 clear_mem_cache().await;
15103
15104 let source = r#"sketch001 = sketch(on = XZ) {
15105 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15106}
15107sketch002 = sketch(on = XY) {
15108 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15109 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15110 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15111 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15112 coincident([line1.end, line2.start])
15113 coincident([line2.end, line3.start])
15114 coincident([line3.end, line4.start])
15115 coincident([line4.end, line1.start])
15116 parallel([line2, line4])
15117 parallel([line3, line1])
15118 perpendicular([line1, line2])
15119 horizontal(line3)
15120 coincident([line1.start, ORIGIN])
15121}
15122"#;
15123
15124 let program = Program::parse(source).unwrap().0.unwrap();
15125 let mut frontend = FrontendState::new();
15126 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15127 let mock_ctx = ExecutorContext::new_mock(None).await;
15128 let version = Version(0);
15129 let project_id = ProjectId(0);
15130 let file_id = FileId(0);
15131
15132 frontend.hack_set_program(&ctx, program).await.unwrap();
15133 let sketch_objects = frontend
15134 .scene_graph
15135 .objects
15136 .iter()
15137 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15138 .collect::<Vec<_>>();
15139 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15140
15141 let sketch1_id = sketch_objects[0].id;
15142 let sketch2_id = sketch_objects[1].id;
15143
15144 frontend
15145 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15146 .await
15147 .unwrap();
15148 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15149
15150 let scene_delta = frontend
15151 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15152 .await
15153 .unwrap();
15154 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15155
15156 clear_mem_cache().await;
15157 ctx.close().await;
15158 mock_ctx.close().await;
15159 }
15160
15161 #[tokio::test(flavor = "multi_thread")]
15166 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15167 let initial_source = "@settings(defaultLengthUnit = mm)
15169
15170sketch001 = sketch(on = XY) {
15171 point(at = [1in, 2in])
15172}
15173";
15174
15175 let program = Program::parse(initial_source).unwrap().0.unwrap();
15176 let mut frontend = FrontendState::new();
15177
15178 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15179 let mock_ctx = ExecutorContext::new_mock(None).await;
15180 let version = Version(0);
15181 let project_id = ProjectId(0);
15182 let file_id = FileId(0);
15183
15184 frontend.hack_set_program(&ctx, program).await.unwrap();
15185 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15186 let sketch_id = sketch_object.id;
15187
15188 frontend
15190 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15191 .await
15192 .unwrap();
15193
15194 let point_ctor = PointCtor {
15196 position: Point2d {
15197 x: Expr::Number(Number {
15198 value: 5.0,
15199 units: NumericSuffix::Mm,
15200 }),
15201 y: Expr::Number(Number {
15202 value: 6.0,
15203 units: NumericSuffix::Mm,
15204 }),
15205 },
15206 };
15207 let segment = SegmentCtor::Point(point_ctor);
15208 let (src_delta, scene_delta) = frontend
15209 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15210 .await
15211 .unwrap();
15212 assert!(
15214 src_delta.text.contains("point(at = [5mm, 6mm])"),
15215 "Expected new point in source, got: {}",
15216 src_delta.text
15217 );
15218 assert!(!scene_delta.new_objects.is_empty());
15219
15220 ctx.close().await;
15221 mock_ctx.close().await;
15222 }
15223
15224 #[tokio::test(flavor = "multi_thread")]
15225 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15226 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15227
15228 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15229 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15230
15231 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15232 assert!(
15233 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15234 "Expected experimental settings to be added to source"
15235 );
15236 }
15237
15238 #[tokio::test(flavor = "multi_thread")]
15239 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15240 let initial_source = "@settings(defaultLengthUnit = mm)
15242
15243s = sketch(on = XY) {}
15244";
15245
15246 let program = Program::parse(initial_source).unwrap().0.unwrap();
15247 let mut frontend = FrontendState::new();
15248
15249 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15250 let mock_ctx = ExecutorContext::new_mock(None).await;
15251 let version = Version(0);
15252
15253 frontend.hack_set_program(&ctx, program).await.unwrap();
15254 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15255 let sketch_id = sketch_object.id;
15256
15257 let line_ctor = LineCtor {
15258 start: Point2d {
15259 x: Expr::Number(Number {
15260 value: 0.0,
15261 units: NumericSuffix::Mm,
15262 }),
15263 y: Expr::Number(Number {
15264 value: 0.0,
15265 units: NumericSuffix::Mm,
15266 }),
15267 },
15268 end: Point2d {
15269 x: Expr::Number(Number {
15270 value: 10.0,
15271 units: NumericSuffix::Mm,
15272 }),
15273 y: Expr::Number(Number {
15274 value: 10.0,
15275 units: NumericSuffix::Mm,
15276 }),
15277 },
15278 construction: None,
15279 };
15280 let segment = SegmentCtor::Line(line_ctor);
15281 let (src_delta, scene_delta) = frontend
15282 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15283 .await
15284 .unwrap();
15285 assert!(
15286 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15287 "Expected line in source, got: {}",
15288 src_delta.text
15289 );
15290 assert_eq!(scene_delta.new_objects.len(), 3);
15292
15293 ctx.close().await;
15294 mock_ctx.close().await;
15295 }
15296
15297 #[tokio::test(flavor = "multi_thread")]
15298 async fn test_extra_newlines_between_operations_edit_line() {
15299 let initial_source = "@settings(defaultLengthUnit = mm)
15301
15302sketch001 = sketch(on = XY) {
15303
15304 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15305
15306}
15307";
15308
15309 let program = Program::parse(initial_source).unwrap().0.unwrap();
15310 let mut frontend = FrontendState::new();
15311
15312 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15313 let mock_ctx = ExecutorContext::new_mock(None).await;
15314 let version = Version(0);
15315 let project_id = ProjectId(0);
15316 let file_id = FileId(0);
15317
15318 let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
15319 assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
15320 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15321 let sketch_id = sketch_object.id;
15322 let sketch = expect_sketch(sketch_object);
15323
15324 let line_id = sketch
15326 .segments
15327 .iter()
15328 .copied()
15329 .find(|seg_id| {
15330 matches!(
15331 &frontend.scene_graph.objects[seg_id.0].kind,
15332 ObjectKind::Segment {
15333 segment: Segment::Line(_)
15334 }
15335 )
15336 })
15337 .expect("Expected a line segment in sketch");
15338
15339 frontend
15341 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15342 .await
15343 .unwrap();
15344
15345 let line_ctor = LineCtor {
15347 start: Point2d {
15348 x: Expr::Var(Number {
15349 value: 1.0,
15350 units: NumericSuffix::Mm,
15351 }),
15352 y: Expr::Var(Number {
15353 value: 2.0,
15354 units: NumericSuffix::Mm,
15355 }),
15356 },
15357 end: Point2d {
15358 x: Expr::Var(Number {
15359 value: 13.0,
15360 units: NumericSuffix::Mm,
15361 }),
15362 y: Expr::Var(Number {
15363 value: 14.0,
15364 units: NumericSuffix::Mm,
15365 }),
15366 },
15367 construction: None,
15368 };
15369 let segments = vec![ExistingSegmentCtor {
15370 id: line_id,
15371 ctor: SegmentCtor::Line(line_ctor),
15372 }];
15373 let (src_delta, _scene_delta) = frontend
15374 .edit_segments(&mock_ctx, version, sketch_id, segments)
15375 .await
15376 .unwrap();
15377 assert!(
15378 src_delta
15379 .text
15380 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15381 "Expected edited line in source, got: {}",
15382 src_delta.text
15383 );
15384
15385 ctx.close().await;
15386 mock_ctx.close().await;
15387 }
15388
15389 #[tokio::test(flavor = "multi_thread")]
15390 async fn test_extra_newlines_delete_segment() {
15391 let initial_source = "@settings(defaultLengthUnit = mm)
15393
15394sketch001 = sketch(on = XY) {
15395 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15396}
15397";
15398
15399 let program = Program::parse(initial_source).unwrap().0.unwrap();
15400 let mut frontend = FrontendState::new();
15401
15402 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15403 let mock_ctx = ExecutorContext::new_mock(None).await;
15404 let version = Version(0);
15405
15406 frontend.hack_set_program(&ctx, program).await.unwrap();
15407 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15408 let sketch_id = sketch_object.id;
15409 let sketch = expect_sketch(sketch_object);
15410
15411 assert_eq!(sketch.segments.len(), 3);
15413 let circle_id = sketch.segments[2];
15414
15415 let (src_delta, scene_delta) = frontend
15417 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15418 .await
15419 .unwrap();
15420 assert!(
15421 src_delta.text.contains("sketch(on = XY) {"),
15422 "Expected sketch block in source, got: {}",
15423 src_delta.text
15424 );
15425 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15426 let new_sketch = expect_sketch(new_sketch_object);
15427 assert_eq!(new_sketch.segments.len(), 0);
15428
15429 ctx.close().await;
15430 mock_ctx.close().await;
15431 }
15432
15433 #[tokio::test(flavor = "multi_thread")]
15434 async fn test_unformatted_source_add_arc() {
15435 let initial_source = "@settings(defaultLengthUnit = mm)
15437
15438sketch001 = sketch(on = XY) {
15439}
15440";
15441
15442 let program = Program::parse(initial_source).unwrap().0.unwrap();
15443 let mut frontend = FrontendState::new();
15444
15445 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15446 let mock_ctx = ExecutorContext::new_mock(None).await;
15447 let version = Version(0);
15448
15449 frontend.hack_set_program(&ctx, program).await.unwrap();
15450 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15451 let sketch_id = sketch_object.id;
15452
15453 let arc_ctor = ArcCtor {
15454 start: Point2d {
15455 x: Expr::Var(Number {
15456 value: 5.0,
15457 units: NumericSuffix::Mm,
15458 }),
15459 y: Expr::Var(Number {
15460 value: 0.0,
15461 units: NumericSuffix::Mm,
15462 }),
15463 },
15464 end: Point2d {
15465 x: Expr::Var(Number {
15466 value: 0.0,
15467 units: NumericSuffix::Mm,
15468 }),
15469 y: Expr::Var(Number {
15470 value: 5.0,
15471 units: NumericSuffix::Mm,
15472 }),
15473 },
15474 center: Point2d {
15475 x: Expr::Var(Number {
15476 value: 0.0,
15477 units: NumericSuffix::Mm,
15478 }),
15479 y: Expr::Var(Number {
15480 value: 0.0,
15481 units: NumericSuffix::Mm,
15482 }),
15483 },
15484 direction: None,
15485 construction: None,
15486 };
15487 let segment = SegmentCtor::Arc(arc_ctor);
15488 let (src_delta, scene_delta) = frontend
15489 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15490 .await
15491 .unwrap();
15492 assert!(
15493 src_delta
15494 .text
15495 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15496 "Expected arc in source, got: {}",
15497 src_delta.text
15498 );
15499 assert!(!scene_delta.new_objects.is_empty());
15500
15501 ctx.close().await;
15502 mock_ctx.close().await;
15503 }
15504
15505 #[tokio::test(flavor = "multi_thread")]
15506 async fn test_arc_direction_flows_to_source() {
15507 let initial_source = "@settings(defaultLengthUnit = mm)
15508
15509sketch001 = sketch(on = XY) {
15510}
15511";
15512
15513 let program = Program::parse(initial_source).unwrap().0.unwrap();
15514 let mut frontend = FrontendState::new();
15515
15516 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15517 let mock_ctx = ExecutorContext::new_mock(None).await;
15518 let version = Version(0);
15519
15520 frontend.hack_set_program(&ctx, program).await.unwrap();
15521 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15522 let sketch_id = sketch_object.id;
15523
15524 let point = |x: f64, y: f64| Point2d {
15525 x: Expr::Var(Number {
15526 value: x,
15527 units: NumericSuffix::Mm,
15528 }),
15529 y: Expr::Var(Number {
15530 value: y,
15531 units: NumericSuffix::Mm,
15532 }),
15533 };
15534
15535 let arc_ctor = ArcCtor {
15537 start: point(5.0, 0.0),
15538 end: point(0.0, 5.0),
15539 center: point(0.0, 0.0),
15540 direction: Some(ArcDirection::Cw),
15541 construction: None,
15542 };
15543 let (src_delta, scene_delta) = frontend
15544 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15545 .await
15546 .unwrap();
15547 assert!(
15548 src_delta.text.contains("direction = CW"),
15549 "Expected direction = CW in source, got: {}",
15550 src_delta.text
15551 );
15552 let arc_id = *scene_delta.new_objects.last().unwrap();
15554
15555 let edited_ctor = ArcCtor {
15559 start: point(0.0, -5.0),
15560 end: point(0.0, 5.0),
15561 center: point(0.0, 0.0),
15562 direction: Some(ArcDirection::Cw),
15563 construction: None,
15564 };
15565 let (src_delta, _scene_delta) = frontend
15566 .edit_segments(
15567 &mock_ctx,
15568 version,
15569 sketch_id,
15570 vec![ExistingSegmentCtor {
15571 id: arc_id,
15572 ctor: SegmentCtor::Arc(edited_ctor),
15573 }],
15574 )
15575 .await
15576 .unwrap();
15577 assert!(
15578 src_delta.text.contains("start = [var 0mm, var -5mm]"),
15579 "Expected edited start point in source, got: {}",
15580 src_delta.text
15581 );
15582 assert!(
15583 src_delta.text.contains("direction = CW"),
15584 "Expected direction = CW to be preserved in source, got: {}",
15585 src_delta.text
15586 );
15587
15588 let edited_ctor = ArcCtor {
15591 start: point(0.0, -5.0),
15592 end: point(0.0, 5.0),
15593 center: point(0.0, 0.0),
15594 direction: Some(ArcDirection::Ccw),
15595 construction: None,
15596 };
15597 let (src_delta, _scene_delta) = frontend
15598 .edit_segments(
15599 &mock_ctx,
15600 version,
15601 sketch_id,
15602 vec![ExistingSegmentCtor {
15603 id: arc_id,
15604 ctor: SegmentCtor::Arc(edited_ctor),
15605 }],
15606 )
15607 .await
15608 .unwrap();
15609 assert!(
15610 !src_delta.text.contains("direction"),
15611 "Expected direction argument to be removed from source, got: {}",
15612 src_delta.text
15613 );
15614
15615 ctx.close().await;
15616 mock_ctx.close().await;
15617 }
15618
15619 #[tokio::test(flavor = "multi_thread")]
15620 async fn test_extra_newlines_add_circle() {
15621 let initial_source = "@settings(defaultLengthUnit = mm)
15623
15624sketch001 = sketch(on = XY) {
15625}
15626";
15627
15628 let program = Program::parse(initial_source).unwrap().0.unwrap();
15629 let mut frontend = FrontendState::new();
15630
15631 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15632 let mock_ctx = ExecutorContext::new_mock(None).await;
15633 let version = Version(0);
15634
15635 frontend.hack_set_program(&ctx, program).await.unwrap();
15636 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15637 let sketch_id = sketch_object.id;
15638
15639 let circle_ctor = CircleCtor {
15640 start: Point2d {
15641 x: Expr::Var(Number {
15642 value: 5.0,
15643 units: NumericSuffix::Mm,
15644 }),
15645 y: Expr::Var(Number {
15646 value: 0.0,
15647 units: NumericSuffix::Mm,
15648 }),
15649 },
15650 center: Point2d {
15651 x: Expr::Var(Number {
15652 value: 0.0,
15653 units: NumericSuffix::Mm,
15654 }),
15655 y: Expr::Var(Number {
15656 value: 0.0,
15657 units: NumericSuffix::Mm,
15658 }),
15659 },
15660 construction: None,
15661 };
15662 let segment = SegmentCtor::Circle(circle_ctor);
15663 let (src_delta, scene_delta) = frontend
15664 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15665 .await
15666 .unwrap();
15667 assert!(
15668 src_delta
15669 .text
15670 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15671 "Expected circle in source, got: {}",
15672 src_delta.text
15673 );
15674 assert!(!scene_delta.new_objects.is_empty());
15675
15676 ctx.close().await;
15677 mock_ctx.close().await;
15678 }
15679
15680 #[tokio::test(flavor = "multi_thread")]
15681 async fn test_extra_newlines_add_constraint() {
15682 let initial_source = "@settings(defaultLengthUnit = mm)
15684
15685sketch001 = sketch(on = XY) {
15686 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15687 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15688}
15689";
15690
15691 let program = Program::parse(initial_source).unwrap().0.unwrap();
15692 let mut frontend = FrontendState::new();
15693
15694 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15695 let mock_ctx = ExecutorContext::new_mock(None).await;
15696 let version = Version(0);
15697 let project_id = ProjectId(0);
15698 let file_id = FileId(0);
15699
15700 frontend.hack_set_program(&ctx, program).await.unwrap();
15701 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15702 let sketch_id = sketch_object.id;
15703 let sketch = expect_sketch(sketch_object);
15704
15705 let line_ids: Vec<ObjectId> = sketch
15707 .segments
15708 .iter()
15709 .copied()
15710 .filter(|seg_id| {
15711 matches!(
15712 &frontend.scene_graph.objects[seg_id.0].kind,
15713 ObjectKind::Segment {
15714 segment: Segment::Line(_)
15715 }
15716 )
15717 })
15718 .collect();
15719 assert_eq!(line_ids.len(), 2, "Expected two line segments");
15720
15721 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15722 let ObjectKind::Segment {
15723 segment: Segment::Line(line1_data),
15724 } = &line1.kind
15725 else {
15726 panic!("Expected line");
15727 };
15728 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15729 let ObjectKind::Segment {
15730 segment: Segment::Line(line2_data),
15731 } = &line2.kind
15732 else {
15733 panic!("Expected line");
15734 };
15735
15736 let constraint = Constraint::Coincident(Coincident {
15738 segments: vec![line1_data.end.into(), line2_data.start.into()],
15739 });
15740
15741 frontend
15743 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15744 .await
15745 .unwrap();
15746 let (src_delta, _scene_delta) = frontend
15747 .add_constraint(&mock_ctx, version, sketch_id, constraint)
15748 .await
15749 .unwrap();
15750 assert!(
15751 src_delta.text.contains("coincident("),
15752 "Expected coincident constraint in source, got: {}",
15753 src_delta.text
15754 );
15755
15756 ctx.close().await;
15757 mock_ctx.close().await;
15758 }
15759
15760 #[tokio::test(flavor = "multi_thread")]
15761 async fn test_extra_newlines_add_line_then_edit_line() {
15762 let initial_source = "@settings(defaultLengthUnit = mm)
15764
15765sketch001 = sketch(on = XY) {
15766}
15767";
15768
15769 let program = Program::parse(initial_source).unwrap().0.unwrap();
15770 let mut frontend = FrontendState::new();
15771
15772 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15773 let mock_ctx = ExecutorContext::new_mock(None).await;
15774 let version = Version(0);
15775
15776 frontend.hack_set_program(&ctx, program).await.unwrap();
15777 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15778 let sketch_id = sketch_object.id;
15779
15780 let line_ctor = LineCtor {
15782 start: Point2d {
15783 x: Expr::Number(Number {
15784 value: 0.0,
15785 units: NumericSuffix::Mm,
15786 }),
15787 y: Expr::Number(Number {
15788 value: 0.0,
15789 units: NumericSuffix::Mm,
15790 }),
15791 },
15792 end: Point2d {
15793 x: Expr::Number(Number {
15794 value: 10.0,
15795 units: NumericSuffix::Mm,
15796 }),
15797 y: Expr::Number(Number {
15798 value: 10.0,
15799 units: NumericSuffix::Mm,
15800 }),
15801 },
15802 construction: None,
15803 };
15804 let segment = SegmentCtor::Line(line_ctor);
15805 let (src_delta, scene_delta) = frontend
15806 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15807 .await
15808 .unwrap();
15809 assert!(
15810 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15811 "Expected line in source after add, got: {}",
15812 src_delta.text
15813 );
15814 let line_id = *scene_delta.new_objects.last().unwrap();
15816
15817 let line_ctor = LineCtor {
15819 start: Point2d {
15820 x: Expr::Number(Number {
15821 value: 1.0,
15822 units: NumericSuffix::Mm,
15823 }),
15824 y: Expr::Number(Number {
15825 value: 2.0,
15826 units: NumericSuffix::Mm,
15827 }),
15828 },
15829 end: Point2d {
15830 x: Expr::Number(Number {
15831 value: 13.0,
15832 units: NumericSuffix::Mm,
15833 }),
15834 y: Expr::Number(Number {
15835 value: 14.0,
15836 units: NumericSuffix::Mm,
15837 }),
15838 },
15839 construction: None,
15840 };
15841 let segments = vec![ExistingSegmentCtor {
15842 id: line_id,
15843 ctor: SegmentCtor::Line(line_ctor),
15844 }];
15845 let (src_delta, scene_delta) = frontend
15846 .edit_segments(&mock_ctx, version, sketch_id, segments)
15847 .await
15848 .unwrap();
15849 assert!(
15850 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15851 "Expected edited line in source, got: {}",
15852 src_delta.text
15853 );
15854 assert_eq!(scene_delta.new_objects, vec![]);
15855
15856 ctx.close().await;
15857 mock_ctx.close().await;
15858 }
15859
15860 #[test]
15861 fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15862 let code = "\
15871foo1 = 1
15872sk = sketch() {
15873 p = var 1.5
15874}
158757 + 8
15876";
15877 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15878 let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15879 panic!("expected an expression statement");
15880 };
15881 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15882 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15883 &mut ast,
15884 &source_ref,
15885 AstMutateCommand::AddVariableDeclaration {
15886 prefix: "foo".to_owned(),
15887 },
15888 )
15889 .unwrap();
15890 let AstMutateCommandReturn::Name(name) = cmd_return else {
15891 panic!("expected a generated name");
15892 };
15893 assert_eq!(name, "foo2");
15894 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15895 panic!("expected the expression statement to become a variable declaration");
15896 };
15897 assert_eq!(decl.name(), "foo2");
15898 }
15899
15900 fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15902 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15903 panic!("expected a variable declaration");
15904 };
15905 let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15906 panic!("expected a function expression");
15907 };
15908 &func.body
15909 }
15910
15911 fn then_block_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15914 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15915 panic!("expected a variable declaration");
15916 };
15917 let ast::Expr::IfExpression(if_expr) = &decl.declaration.init else {
15918 panic!("expected an if expression");
15919 };
15920 &if_expr.then_val
15921 }
15922
15923 #[test]
15924 fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15925 let code = "\
15930fn build() {
15931 thing1 = 1
15932 10 + 20
15933 return thing1
15934}
15935";
15936 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15937 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15938 panic!("expected an expression statement");
15939 };
15940 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15941 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15942 &mut ast,
15943 &source_ref,
15944 AstMutateCommand::AddVariableDeclaration {
15945 prefix: "thing".to_owned(),
15946 },
15947 )
15948 .unwrap();
15949 let AstMutateCommandReturn::Name(name) = cmd_return else {
15950 panic!("expected a generated name");
15951 };
15952 assert_eq!(name, "thing2");
15953 let body = &function_body_at(&ast, 0).body;
15954 assert_eq!(body.len(), 3);
15955 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
15956 panic!("expected the expression statement to become a variable declaration");
15957 };
15958 assert_eq!(decl.name(), "thing2");
15959 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15961 panic!("expected a variable declaration");
15962 };
15963 assert_eq!(first.name(), "thing1");
15964 assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
15965 }
15966
15967 #[test]
15968 fn test_delete_node_in_function_body_preserves_leading_comment() {
15969 let code = "\
15973fn build() {
15974 a = 1
15975 // keep me
15976 b = 2
15977 return a
15978}
15979";
15980 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15981 let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
15982 panic!("expected a variable declaration");
15983 };
15984 assert_eq!(b_decl.name(), "b");
15985 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
15986 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
15987 let body = &function_body_at(&ast, 0).body;
15988 assert_eq!(body.len(), 2, "expected b to be deleted");
15989 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15990 panic!("expected a variable declaration");
15991 };
15992 assert_eq!(first.name(), "a");
15993 let ast::BodyItem::ReturnStatement(_) = &body[1] else {
15994 panic!("expected the return statement to remain");
15995 };
15996 assert!(
15997 body[1].get_comments().iter().any(|c| c.contains("keep me")),
15998 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
15999 body[1].get_comments()
16000 );
16001 }
16002
16003 #[test]
16004 fn test_add_variable_declaration_in_function_body_ignores_parameters() {
16005 let code = "\
16013fn build(thing2) {
16014 thing1 = 1
16015 10 + 20
16016 return thing1 + thing2
16017}
16018";
16019 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16020 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16021 panic!("expected an expression statement");
16022 };
16023 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16024 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16025 &mut ast,
16026 &source_ref,
16027 AstMutateCommand::AddVariableDeclaration {
16028 prefix: "thing".to_owned(),
16029 },
16030 )
16031 .unwrap();
16032 let AstMutateCommandReturn::Name(name) = cmd_return else {
16033 panic!("expected a generated name");
16034 };
16035 assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16036 }
16037
16038 #[test]
16039 fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16040 let code = "\
16041x = 1
16042y = if x > 0 {
16043 q1 = 1
16044 foo(q1)
16045 q1
16046} else {
16047 2
16048}
16049";
16050 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16051 let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16052 panic!("expected an expression statement");
16053 };
16054 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16055 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16056 &mut ast,
16057 &source_ref,
16058 AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16059 )
16060 .unwrap();
16061 let AstMutateCommandReturn::Name(name) = cmd_return else {
16062 panic!("expected a generated name");
16063 };
16064 assert_eq!(name, "q2");
16065 let body = &then_block_at(&ast, 1).body;
16066 assert_eq!(body.len(), 3);
16067 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16068 panic!("expected the expression statement to become a variable declaration");
16069 };
16070 assert_eq!(decl.name(), "q2");
16071 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16073 panic!("expected a variable declaration");
16074 };
16075 assert_eq!(first.name(), "q1");
16076 assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16077 }
16078
16079 #[test]
16080 fn test_delete_node_in_if_branch_preserves_leading_comment() {
16081 let code = "\
16084y = if true {
16085 a = 1
16086 // keep me
16087 b = 2
16088 a + b
16089} else {
16090 2
16091}
16092";
16093 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16094 let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16095 panic!("expected a variable declaration");
16096 };
16097 assert_eq!(b_decl.name(), "b");
16098 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16099 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16100 let body = &then_block_at(&ast, 0).body;
16101 assert_eq!(body.len(), 2, "expected b to be deleted");
16102 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16103 panic!("expected a variable declaration");
16104 };
16105 assert_eq!(first.name(), "a");
16106 let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16107 panic!("expected the tail expression to remain");
16108 };
16109 assert!(
16110 body[1].get_comments().iter().any(|c| c.contains("keep me")),
16111 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16112 body[1].get_comments()
16113 );
16114 }
16115}