1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_api::UnitLength;
9use kcl_error::CompilationIssue;
10use kcl_error::SourceRange;
11use serde::Serialize;
12use uuid::Uuid;
13
14use crate::ExecOutcome;
15use crate::ExecutorContext;
16use crate::KclError;
17use crate::KclErrorWithOutputs;
18use crate::KclValueView;
19use crate::Program;
20use crate::SegmentDragAnchor;
21use crate::collections::AhashIndexSet;
22use crate::execution::Artifact;
23use crate::execution::ArtifactGraph;
24use crate::execution::ArtifactId;
25use crate::execution::CapSubType;
26use crate::execution::CodeRef;
27use crate::execution::MockConfig;
28use crate::execution::SKETCH_BLOCK_PARAM_ON;
29use crate::execution::annotations::WarningLevel;
30use crate::execution::cache::SketchModeState;
31use crate::execution::cache::clear_mem_cache;
32use crate::execution::cache::read_old_memory;
33use crate::execution::cache::write_old_memory;
34use crate::execution::types::adjust_length;
35use crate::fmt::format_number_literal;
36use crate::front::Angle;
37use crate::front::ArcCtor;
38use crate::front::ArcDirection;
39use crate::front::CircleCtor;
40use crate::front::ControlPointSplineCtor;
41use crate::front::Distance;
42use crate::front::EqualRadius;
43use crate::front::Error;
44use crate::front::ExecResult;
45use crate::front::FixedPoint;
46use crate::front::Freedom;
47use crate::front::LinesEqualLength;
48use crate::front::Midpoint;
49use crate::front::Object;
50use crate::front::Parallel;
51use crate::front::Perpendicular;
52use crate::front::PointCtor;
53use crate::front::Symmetric;
54use crate::front::Tangent;
55use crate::frontend::api::CapSource;
56use crate::frontend::api::Expr;
57use crate::frontend::api::FileId;
58use crate::frontend::api::Number;
59use crate::frontend::api::ObjectId;
60use crate::frontend::api::ObjectKind;
61use crate::frontend::api::Plane;
62use crate::frontend::api::ProjectId;
63use crate::frontend::api::RestoreSketchCheckpointOutcome;
64use crate::frontend::api::SceneGraph;
65use crate::frontend::api::SceneGraphDelta;
66use crate::frontend::api::SketchCheckpointId;
67use crate::frontend::api::SourceDelta;
68use crate::frontend::api::SourceRef;
69use crate::frontend::api::SourceRefRange;
70use crate::frontend::api::Version;
71use crate::frontend::api::WallSource;
72use crate::frontend::modify::find_defined_names;
73use crate::frontend::modify::next_free_name;
74use crate::frontend::modify::next_free_name_with_padding;
75use crate::frontend::sketch::Coincident;
76use crate::frontend::sketch::Constraint;
77use crate::frontend::sketch::ConstraintLabelPositionEdit;
78use crate::frontend::sketch::ConstraintSegment;
79use crate::frontend::sketch::Diameter;
80use crate::frontend::sketch::ExistingSegmentCtor;
81use crate::frontend::sketch::Horizontal;
82use crate::frontend::sketch::LineCtor;
83use crate::frontend::sketch::Point2d;
84use crate::frontend::sketch::Radius;
85use crate::frontend::sketch::Segment;
86use crate::frontend::sketch::SegmentCtor;
87use crate::frontend::sketch::SketchApi;
88use crate::frontend::sketch::SketchCtor;
89use crate::frontend::sketch::Vertical;
90use crate::id::IncIdGenerator;
91use crate::parsing::ast::types as ast;
92use crate::parsing::ast::types::BoxNode;
93use crate::parsing::ast::types::CallExpressionKw;
94use crate::parsing::ast::types::NodePathExt;
95use crate::pretty::NumericSuffix;
96use crate::std::constraints::LinesAtAngleKind;
97use crate::walk::NodeMut;
98use crate::walk::Visitable;
99use crate::walk::traverse::MutateBodyItem;
100use crate::walk::traverse::TraversalReturn;
101use crate::walk::traverse::Visitor;
102use crate::walk::traverse::dfs_mut;
103
104pub(crate) mod api;
105pub(crate) mod modify;
106pub(crate) mod sketch;
107
108pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
109
110#[derive(Debug, Clone)]
111struct SketchCheckpoint {
112 id: SketchCheckpointId,
113 source: SourceDelta,
114 program: Program,
115 scene_graph: SceneGraph,
116 exec_outcome: ExecOutcome,
117 point_freedom_cache: HashMap<ObjectId, Freedom>,
118 mock_memory: Option<SketchModeState>,
119}
120pub(crate) mod trim;
121
122struct ArcSizeConstraintParams {
123 points: Vec<ObjectId>,
124 function_name: &'static str,
125 value: f64,
126 units: NumericSuffix,
127 label_position: Option<Point2d<Number>>,
128 constraint_type_name: &'static str,
129}
130
131const POINT_FN: &str = "point";
132const POINT_AT_PARAM: &str = "at";
133const LINE_FN: &str = "line";
134const LINE_VARIABLE: &str = "line";
135const LINE_START_PARAM: &str = "start";
136const LINE_END_PARAM: &str = "end";
137const ARC_FN: &str = "arc";
138const ARC_VARIABLE: &str = "arc";
139const ARC_START_PARAM: &str = "start";
140const ARC_END_PARAM: &str = "end";
141const ARC_CENTER_PARAM: &str = "center";
142const ARC_DIRECTION_PARAM: &str = "direction";
143const ARC_DIRECTION_CW_NAME: &str = "CW";
145const CIRCLE_FN: &str = "circle";
146const CIRCLE_VARIABLE: &str = "circle";
147const CIRCLE_START_PARAM: &str = "start";
148const CIRCLE_CENTER_PARAM: &str = "center";
149const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
150const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
151const LABEL_POSITION_PARAM: &str = "labelPosition";
152
153const COINCIDENT_FN: &str = "coincident";
154const DIAMETER_FN: &str = "diameter";
155const DISTANCE_FN: &str = "distance";
156const FIXED_FN: &str = "fixed";
157const ANGLE_FN: &str = "angle";
158const ANGLE_DIMENSION_FN: &str = "angleDimension";
159const ANGLE_LINES_PARAM: &str = "lines";
160const ANGLE_SECTOR_PARAM: &str = "sector";
161const ANGLE_INVERSE_PARAM: &str = "inverse";
162const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
163const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
164const EQUAL_LENGTH_FN: &str = "equalLength";
165const EQUAL_RADIUS_FN: &str = "equalRadius";
166const HORIZONTAL_FN: &str = "horizontal";
167const MIDPOINT_FN: &str = "midpoint";
168const MIDPOINT_POINT_PARAM: &str = "point";
169const RADIUS_FN: &str = "radius";
170const SYMMETRIC_FN: &str = "symmetric";
171const SYMMETRIC_AXIS_PARAM: &str = "axis";
172const TANGENT_FN: &str = "tangent";
173const VERTICAL_FN: &str = "vertical";
174
175const LINE_PROPERTY_START: &str = "start";
176const LINE_PROPERTY_END: &str = "end";
177
178const ARC_PROPERTY_START: &str = "start";
179const ARC_PROPERTY_END: &str = "end";
180const ARC_PROPERTY_CENTER: &str = "center";
181const CIRCLE_PROPERTY_START: &str = "start";
182const CIRCLE_PROPERTY_CENTER: &str = "center";
183const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
184const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
185
186const CONSTRUCTION_PARAM: &str = "construction";
187
188#[derive(Debug, Clone, Copy)]
189enum EditDeleteKind {
190 Edit,
191 DeleteNonSketch,
192}
193
194struct ExecuteAfterEditOptions {
196 segment_ids_edited: AhashIndexSet<ObjectId>,
197 edit_kind: EditDeleteKind,
198 commit_solved_initial_guesses: bool,
199}
200
201impl EditDeleteKind {
202 fn is_delete(&self) -> bool {
204 match self {
205 EditDeleteKind::Edit => false,
206 EditDeleteKind::DeleteNonSketch => true,
207 }
208 }
209
210 fn to_change_kind(self) -> ChangeKind {
211 match self {
212 EditDeleteKind::Edit => ChangeKind::Edit,
213 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
214 }
215 }
216}
217
218#[derive(Debug, Clone, Copy)]
219enum ChangeKind {
220 Add,
221 Edit,
222 Delete,
223 None,
224}
225
226#[derive(Debug, Clone, Serialize, ts_rs::TS)]
227#[ts(export, export_to = "FrontendApi.ts")]
228#[serde(tag = "type")]
229pub enum SetProgramOutcome {
230 #[serde(rename_all = "camelCase")]
231 Success {
232 scene_graph: Box<SceneGraph>,
233 exec_outcome: Box<ExecOutcome>,
234 checkpoint_id: Option<SketchCheckpointId>,
235 },
236 #[serde(rename_all = "camelCase")]
237 ExecFailure { error: Box<KclErrorWithOutputs> },
238}
239
240pub struct EditSegmentsOptions {
242 pub anchor_segment_ids: Option<Vec<ObjectId>>,
248 pub drag_anchors: Vec<SegmentDragAnchor>,
251 pub constraint_label_edits: Vec<ConstraintLabelPositionEdit>,
254 pub commit_solved_initial_guesses: bool,
256}
257
258pub struct EditDistanceConstraintLabelPositionOptions {
260 pub anchor_segment_ids: Vec<ObjectId>,
262 pub commit_solved_initial_guesses: bool,
264}
265
266pub struct EditConstraintOptions {
268 pub commit_solved_initial_guesses: bool,
270}
271
272#[derive(Debug, Clone)]
273struct SolidAstReference {
274 variable_name: String,
275 output_index: Option<usize>,
276}
277
278#[derive(Debug, Clone)]
279pub struct FrontendState {
280 program: Program,
281 scene_graph: SceneGraph,
282 solid_references: HashMap<Uuid, SolidAstReference>,
284 point_freedom_cache: HashMap<ObjectId, Freedom>,
287 next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
290 next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
293 next_constraint_label_edits: Option<Vec<ConstraintLabelPositionEdit>>,
295 next_edit_commits_solver_solutions: Option<bool>,
299 sketch_checkpoints: VecDeque<SketchCheckpoint>,
300 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
301}
302
303impl Default for FrontendState {
304 fn default() -> Self {
305 Self::new()
306 }
307}
308
309impl FrontendState {
310 pub fn new() -> Self {
311 Self {
312 program: Program::empty(),
313 scene_graph: SceneGraph {
314 project: ProjectId(0),
315 file: FileId(0),
316 version: Version(0),
317 objects: Default::default(),
318 settings: Default::default(),
319 sketch_mode: Default::default(),
320 },
321 solid_references: HashMap::new(),
322 point_freedom_cache: HashMap::new(),
323 next_drag_anchor_segment_ids: None,
324 next_segment_drag_anchors: None,
325 next_constraint_label_edits: None,
326 next_edit_commits_solver_solutions: None,
327 sketch_checkpoints: VecDeque::new(),
328 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
329 }
330 }
331
332 pub fn scene_graph(&self) -> &SceneGraph {
334 &self.scene_graph
335 }
336
337 pub fn default_length_unit(&self) -> UnitLength {
338 self.program
339 .meta_settings()
340 .ok()
341 .flatten()
342 .map(|settings| settings.default_length_units)
343 .unwrap_or(UnitLength::Millimeters)
344 }
345
346 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
347 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
348
349 let checkpoint = SketchCheckpoint {
350 id: checkpoint_id,
351 source: SourceDelta {
352 text: source_from_ast(&self.program.ast),
353 },
354 program: self.program.clone(),
355 scene_graph: self.scene_graph.clone(),
356 exec_outcome,
357 point_freedom_cache: self.point_freedom_cache.clone(),
358 mock_memory: read_old_memory().await,
359 };
360
361 self.sketch_checkpoints.push_back(checkpoint);
362 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
363 self.sketch_checkpoints.pop_front();
364 }
365
366 Ok(checkpoint_id)
367 }
368
369 pub async fn edit_segments_with_options(
377 &mut self,
378 ctx: &ExecutorContext,
379 version: Version,
380 sketch: ObjectId,
381 segments: Vec<ExistingSegmentCtor>,
382 options: EditSegmentsOptions,
383 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
384 let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
385 self.next_drag_anchor_segment_ids
386 .replace(anchor_ids.into_iter().collect())
387 });
388 let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
389 let previous_constraint_label_edits = self.next_constraint_label_edits.replace(options.constraint_label_edits);
390 let previous_commit_mode = self
391 .next_edit_commits_solver_solutions
392 .replace(options.commit_solved_initial_guesses);
393 let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
394 if let Some(previous_anchor_ids) = previous_anchor_ids {
395 self.next_drag_anchor_segment_ids = previous_anchor_ids;
396 }
397 self.next_segment_drag_anchors = previous_drag_anchors;
398 self.next_constraint_label_edits = previous_constraint_label_edits;
399 self.next_edit_commits_solver_solutions = previous_commit_mode;
400 result
401 }
402
403 pub async fn edit_distance_constraint_label_position_with_options(
409 &mut self,
410 ctx: &ExecutorContext,
411 version: Version,
412 sketch: ObjectId,
413 constraint_id: ObjectId,
414 label_position: Point2d<Number>,
415 options: EditDistanceConstraintLabelPositionOptions,
416 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
417 let previous_commit_mode = self
418 .next_edit_commits_solver_solutions
419 .replace(options.commit_solved_initial_guesses);
420 let result = SketchApi::edit_distance_constraint_label_position(
421 self,
422 ctx,
423 version,
424 sketch,
425 constraint_id,
426 label_position,
427 options.anchor_segment_ids,
428 )
429 .await;
430 self.next_edit_commits_solver_solutions = previous_commit_mode;
431 result
432 }
433
434 pub async fn edit_distance_constraint_with_options(
436 &mut self,
437 ctx: &ExecutorContext,
438 version: Version,
439 sketch: ObjectId,
440 constraint_id: ObjectId,
441 constraint: Constraint,
442 options: EditConstraintOptions,
443 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
444 self.edit_constraint_with_options(ctx, version, sketch, constraint_id, constraint, options)
445 .await
446 }
447
448 pub async fn edit_angle_constraint_with_options(
450 &mut self,
451 ctx: &ExecutorContext,
452 version: Version,
453 sketch: ObjectId,
454 constraint_id: ObjectId,
455 angle: Angle,
456 options: EditConstraintOptions,
457 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
458 self.edit_constraint_with_options(ctx, version, sketch, constraint_id, Constraint::Angle(angle), options)
459 .await
460 }
461
462 pub async fn edit_constraint_with_options(
464 &mut self,
465 ctx: &ExecutorContext,
466 _version: Version,
467 sketch: ObjectId,
468 constraint_id: ObjectId,
469 constraint: Constraint,
470 options: EditConstraintOptions,
471 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
472 let sketch_block_ref =
474 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
475
476 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
477 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
478 })?;
479
480 let mut new_ast = self.program.ast.clone();
481 let command = match &object.kind {
482 ObjectKind::Constraint {
483 constraint:
484 Constraint::Distance(_) | Constraint::HorizontalDistance(_) | Constraint::VerticalDistance(_),
485 } => {
486 let (function_name, distance) = match &constraint {
487 Constraint::Distance(distance) => (DISTANCE_FN, distance),
488 Constraint::HorizontalDistance(distance) => (HORIZONTAL_DISTANCE_FN, distance),
489 Constraint::VerticalDistance(distance) => (VERTICAL_DISTANCE_FN, distance),
490 _ => {
491 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
492 "A distance constraint can only be replaced by another distance constraint".to_owned(),
493 )));
494 }
495 };
496 let (call, value) = self
497 .distance_constraint_ast_parts(function_name, distance, &mut new_ast)
498 .map_err(KclErrorWithOutputs::no_outputs)?;
499 AstMutateCommand::EditDistanceConstraint { call, value }
500 }
501 ObjectKind::Constraint {
502 constraint: Constraint::Angle(_),
503 } => {
504 let Constraint::Angle(angle) = &constraint else {
505 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
506 "An angle constraint can only be replaced by another angle constraint".to_owned(),
507 )));
508 };
509 let (call, value) = self
510 .angle_constraint_ast_parts(angle, &mut new_ast)
511 .map_err(KclErrorWithOutputs::no_outputs)?;
512 AstMutateCommand::EditAngleConstraint { call, value }
513 }
514 ObjectKind::Constraint { .. } => {
515 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
516 "Editing {} is not supported",
517 object.kind.human_friendly_kind_with_article(),
518 ))));
519 }
520 _ => {
521 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
522 "Object is not a constraint: {constraint_id:?}"
523 ))));
524 }
525 };
526
527 self.mutate_ast(&mut new_ast, constraint_id, command)
528 .map_err(KclErrorWithOutputs::no_outputs)?;
529
530 self.execute_after_edit(
531 ctx,
532 sketch,
533 sketch_block_ref,
534 &mut new_ast,
535 ExecuteAfterEditOptions {
536 segment_ids_edited: Default::default(),
537 edit_kind: EditDeleteKind::Edit,
538 commit_solved_initial_guesses: options.commit_solved_initial_guesses,
539 },
540 )
541 .await
542 }
543
544 pub async fn restore_sketch_checkpoint(
545 &mut self,
546 checkpoint_id: SketchCheckpointId,
547 ) -> api::Result<RestoreSketchCheckpointOutcome> {
548 let checkpoint = self
549 .sketch_checkpoints
550 .iter()
551 .find(|checkpoint| checkpoint.id == checkpoint_id)
552 .cloned()
553 .ok_or_else(|| Error {
554 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
555 })?;
556
557 self.program = checkpoint.program;
558 self.scene_graph = checkpoint.scene_graph.clone();
559 self.solid_references = solid_references_from_variables(&self.program.ast, &checkpoint.exec_outcome.variables);
560 self.point_freedom_cache = checkpoint.point_freedom_cache;
561 self.next_drag_anchor_segment_ids = None;
562 self.next_segment_drag_anchors = None;
563 self.next_constraint_label_edits = None;
564 self.next_edit_commits_solver_solutions = None;
565
566 if let Some(mock_memory) = checkpoint.mock_memory {
567 write_old_memory(mock_memory).await;
568 } else {
569 clear_mem_cache().await;
570 }
571
572 Ok(RestoreSketchCheckpointOutcome {
573 source_delta: checkpoint.source,
574 scene_graph_delta: SceneGraphDelta {
575 new_graph: self.scene_graph_for_ui(),
576 new_objects: Vec::new(),
577 invalidates_ids: true,
578 exec_outcome: checkpoint.exec_outcome,
579 },
580 })
581 }
582
583 pub fn clear_sketch_checkpoints(&mut self) {
584 self.sketch_checkpoints.clear();
585 }
586 fn scene_graph_for_ui(&self) -> SceneGraph {
587 let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
588 matches!(
589 object.kind,
590 ObjectKind::Segment {
591 segment: Segment::ControlPointSpline(_)
592 }
593 )
594 });
595
596 if !has_control_point_splines {
597 return self.scene_graph.clone();
598 }
599
600 let hidden_constraint_ids = self
601 .scene_graph
602 .objects
603 .iter()
604 .filter_map(|object| match &object.kind {
605 ObjectKind::Constraint {
606 constraint: Constraint::Coincident(coincident),
607 } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
608 Some(object.id)
609 }
610 _ => None,
611 })
612 .collect::<HashSet<_>>();
613
614 if hidden_constraint_ids.is_empty() {
615 return self.scene_graph.clone();
616 }
617
618 let mut scene_graph = self.scene_graph.clone();
619 for object in &mut scene_graph.objects {
620 match &mut object.kind {
621 ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
622 object.kind = ObjectKind::Nil;
623 }
624 ObjectKind::Sketch(sketch) => {
625 sketch
626 .constraints
627 .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
628 }
629 _ => {}
630 }
631 }
632
633 scene_graph
634 }
635}
636
637fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
638 let mut first_owner_id = None;
639 for segment_id in coincident.segment_ids() {
640 let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
641 return false;
642 };
643
644 match first_owner_id {
645 Some(first_owner_id) if first_owner_id != owner_id => return false,
646 Some(_) => {}
647 None => first_owner_id = Some(owner_id),
648 }
649 }
650
651 first_owner_id.is_some()
652}
653
654fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
655 let object = scene_graph.objects.get(segment_id.0)?;
656 let ObjectKind::Segment { segment } = &object.kind else {
657 return None;
658 };
659
660 match segment {
661 Segment::ControlPointSpline(_) => Some(segment_id),
662 Segment::Point(point) => point
663 .owner
664 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
665 Segment::Line(line) => line
666 .owner
667 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
668 _ => None,
669 }
670}
671
672fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
673 matches!(
674 scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
675 Some(ObjectKind::Segment {
676 segment: Segment::ControlPointSpline(_)
677 })
678 )
679}
680
681fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
682 let experimental_features_allowed = program
683 .meta_settings()
684 .ok()
685 .flatten()
686 .map(|settings| settings.experimental_features == WarningLevel::Allow)
687 .unwrap_or(false);
688 if experimental_features_allowed {
689 return Ok(program.clone());
690 }
691
692 program.change_experimental_features(Some(WarningLevel::Allow))
693}
694
695impl SketchApi for FrontendState {
696 async fn execute_mock(
697 &mut self,
698 ctx: &ExecutorContext,
699 _version: Version,
700 sketch: ObjectId,
701 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
702 let sketch_block_ref =
703 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
704
705 let mut truncated_program = self.program.clone();
706 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
707 .map_err(KclErrorWithOutputs::no_outputs)?;
708
709 let outcome = ctx
711 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
712 .await?;
713 let new_source = source_from_ast(&self.program.ast);
714 let src_delta = SourceDelta { text: new_source };
715 let outcome = self.update_state_after_exec(outcome, true);
717 let scene_graph_delta = SceneGraphDelta {
718 new_graph: self.scene_graph.clone(),
719 new_objects: Default::default(),
720 invalidates_ids: false,
721 exec_outcome: outcome,
722 };
723 Ok((src_delta, scene_graph_delta))
724 }
725
726 async fn new_sketch(
727 &mut self,
728 ctx: &ExecutorContext,
729 _project: ProjectId,
730 _file: FileId,
731 _version: Version,
732 args: SketchCtor,
733 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
734 let mut new_ast = self.program.ast.clone();
737 let mut plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &self.solid_references, &args.on)
739 .map_err(KclErrorWithOutputs::no_outputs)?;
740 let mut defined_names = find_defined_names(&new_ast);
741 let is_face_of_expr = matches!(
742 &plane_ast,
743 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
744 );
745 if is_face_of_expr {
746 let face_name = next_free_name_with_padding("face", &defined_names)
747 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
748 let face_decl = ast::VariableDeclaration::new(
749 ast::VariableDeclarator::new(&face_name, plane_ast),
750 ast::ItemVisibility::Default,
751 ast::VariableKind::Const,
752 );
753 new_ast
754 .body
755 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
756 face_decl,
757 ))));
758 defined_names.insert(face_name.clone());
759 plane_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(&face_name)));
760 }
761 let sketch_ast = ast::SketchBlock {
762 arguments: vec![ast::LabeledArg {
763 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
764 arg: plane_ast,
765 }],
766 body: Default::default(),
767 is_being_edited: false,
768 non_code_meta: Default::default(),
769 digest: None,
770 };
771 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
774 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
775 let sketch_decl = ast::VariableDeclaration::new(
776 ast::VariableDeclarator::new(
777 &sketch_name,
778 ast::Expr::SketchBlock(BoxNode::new(ast::Node::no_src(sketch_ast))),
779 ),
780 ast::ItemVisibility::Default,
781 ast::VariableKind::Const,
782 );
783 new_ast
784 .body
785 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
786 sketch_decl,
787 ))));
788 let new_source = source_from_ast(&new_ast);
790 let new_program = parse_frontend_mutation_source(
792 &new_source,
793 "Error parsing KCL source after adding sketch",
794 "No AST produced after adding sketch",
795 )?;
796
797 self.program = new_program.clone();
799
800 let outcome = ctx.run_with_caching(new_program.clone()).await?;
803 let freedom_analysis_ran = true;
804
805 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
806
807 let Some(sketch_id) = self
808 .scene_graph
809 .objects
810 .iter()
811 .filter_map(|object| match object.kind {
812 ObjectKind::Sketch(_) => Some(object.id),
813 _ => None,
814 })
815 .max_by_key(|id| id.0)
816 else {
817 return Err(KclErrorWithOutputs::from_error_outcome(
818 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
819 outcome,
820 ));
821 };
822 self.scene_graph.sketch_mode = Some(sketch_id);
824
825 let src_delta = SourceDelta { text: new_source };
826 let scene_graph_delta = SceneGraphDelta {
827 new_graph: self.scene_graph_for_ui(),
828 invalidates_ids: false,
829 new_objects: vec![sketch_id],
830 exec_outcome: outcome,
831 };
832 Ok((src_delta, scene_graph_delta, sketch_id))
833 }
834
835 async fn edit_sketch(
836 &mut self,
837 ctx: &ExecutorContext,
838 _project: ProjectId,
839 _file: FileId,
840 _version: Version,
841 sketch: ObjectId,
842 ) -> ExecResult<SceneGraphDelta> {
843 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
847 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
848 })?;
849 let ObjectKind::Sketch(_) = &sketch_object.kind else {
850 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
851 "Object is not a sketch, it is {}",
852 sketch_object.kind.human_friendly_kind_with_article()
853 ))));
854 };
855 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
856
857 self.scene_graph.sketch_mode = Some(sketch);
859
860 let mut truncated_program = self.program.clone();
862 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
863 .map_err(KclErrorWithOutputs::no_outputs)?;
864
865 let outcome = ctx
868 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
869 .await?;
870
871 let outcome = self.update_state_after_exec(outcome, true);
873 let scene_graph_delta = SceneGraphDelta {
874 new_graph: self.scene_graph_for_ui(),
875 invalidates_ids: false,
876 new_objects: Vec::new(),
877 exec_outcome: outcome,
878 };
879 Ok(scene_graph_delta)
880 }
881
882 async fn exit_sketch(
883 &mut self,
884 ctx: &ExecutorContext,
885 _version: Version,
886 sketch: ObjectId,
887 ) -> ExecResult<SceneGraph> {
888 #[cfg(not(target_arch = "wasm32"))]
890 let _ = sketch;
891 #[cfg(target_arch = "wasm32")]
892 if self.scene_graph.sketch_mode != Some(sketch) {
893 web_sys::console::warn_1(
894 &format!(
895 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
896 self.scene_graph.sketch_mode
897 )
898 .into(),
899 );
900 }
901 self.scene_graph.sketch_mode = None;
902
903 let outcome = ctx.run_with_caching(self.program.clone()).await?;
905
906 self.update_state_after_exec(outcome, false);
908
909 Ok(self.scene_graph_for_ui())
910 }
911
912 async fn delete_sketch(
913 &mut self,
914 ctx: &ExecutorContext,
915 _version: Version,
916 sketch: ObjectId,
917 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
918 let mut new_ast = self.program.ast.clone();
921
922 let sketch_id = sketch;
924 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
925 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
926 })?;
927 let ObjectKind::Sketch(_) = &sketch_object.kind else {
928 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
929 "Object is not a sketch, it is {}",
930 sketch_object.kind.human_friendly_kind_with_article(),
931 ))));
932 };
933
934 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
936 .map_err(KclErrorWithOutputs::no_outputs)?;
937
938 self.execute_after_delete_sketch(ctx, &mut new_ast).await
939 }
940
941 async fn add_segment(
942 &mut self,
943 ctx: &ExecutorContext,
944 _version: Version,
945 sketch: ObjectId,
946 segment: SegmentCtor,
947 _label: Option<String>,
948 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
949 match segment {
951 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
952 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
953 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
954 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
955 SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
956 }
957 }
958
959 async fn edit_segments(
960 &mut self,
961 ctx: &ExecutorContext,
962 _version: Version,
963 sketch: ObjectId,
964 segments: Vec<ExistingSegmentCtor>,
965 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
966 let sketch_block_ref =
968 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
969
970 let mut new_ast = self.program.ast.clone();
971 let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
972 let mut invalidates_ids = false;
973
974 for segment in &segments {
977 edited_segment_ids.insert(segment.id);
978 if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
979 && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
980 && let ObjectKind::Segment {
981 segment: Segment::ControlPointSpline(existing_spline),
982 } = &existing_object.kind
983 && existing_spline.controls.len() != new_ctor.points.len()
984 {
985 invalidates_ids = true;
986 }
987 }
988 let drag_anchor_segment_ids = self
989 .next_drag_anchor_segment_ids
990 .take()
991 .unwrap_or_else(|| edited_segment_ids.clone());
992 let constraint_label_edits = self.next_constraint_label_edits.take().unwrap_or_default();
993 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
994
995 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
1010
1011 for segment in segments {
1012 let segment_id = segment.id;
1013 match segment.ctor {
1014 SegmentCtor::Point(ctor) => {
1015 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
1017 && let ObjectKind::Segment { segment } = &segment_object.kind
1018 && let Segment::Point(point) = segment
1019 && let Some(owner_id) = point.owner
1020 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1021 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1022 {
1023 match owner_segment {
1024 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
1025 if let Some(existing) = final_edits.get_mut(&owner_id) {
1026 let SegmentCtor::Line(line_ctor) = existing else {
1027 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1028 "Internal: Expected line ctor for owner, but found {}",
1029 existing.human_friendly_kind_with_article()
1030 ))));
1031 };
1032 if line.start == segment_id {
1034 line_ctor.start = ctor.position;
1035 } else {
1036 line_ctor.end = ctor.position;
1037 }
1038 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
1039 let mut line_ctor = line_ctor.clone();
1041 if line.start == segment_id {
1042 line_ctor.start = ctor.position;
1043 } else {
1044 line_ctor.end = ctor.position;
1045 }
1046 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
1047 } else {
1048 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1050 "Internal: Line does not have line ctor, but found {}",
1051 line.ctor.human_friendly_kind_with_article()
1052 ))));
1053 }
1054 continue;
1055 }
1056 Segment::Arc(arc)
1057 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
1058 {
1059 if let Some(existing) = final_edits.get_mut(&owner_id) {
1060 let SegmentCtor::Arc(arc_ctor) = existing else {
1061 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1062 "Internal: Expected arc ctor for owner, but found {}",
1063 existing.human_friendly_kind_with_article()
1064 ))));
1065 };
1066 if arc.start == segment_id {
1067 arc_ctor.start = ctor.position;
1068 } else if arc.end == segment_id {
1069 arc_ctor.end = ctor.position;
1070 } else {
1071 arc_ctor.center = ctor.position;
1072 }
1073 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
1074 let mut arc_ctor = arc_ctor.clone();
1075 if arc.start == segment_id {
1076 arc_ctor.start = ctor.position;
1077 } else if arc.end == segment_id {
1078 arc_ctor.end = ctor.position;
1079 } else {
1080 arc_ctor.center = ctor.position;
1081 }
1082 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
1083 } else {
1084 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1085 "Internal: Arc does not have arc ctor, but found {}",
1086 arc.ctor.human_friendly_kind_with_article()
1087 ))));
1088 }
1089 continue;
1090 }
1091 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
1092 if let Some(existing) = final_edits.get_mut(&owner_id) {
1093 let SegmentCtor::Circle(circle_ctor) = existing else {
1094 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1095 "Internal: Expected circle ctor for owner, but found {}",
1096 existing.human_friendly_kind_with_article()
1097 ))));
1098 };
1099 if circle.start == segment_id {
1100 circle_ctor.start = ctor.position;
1101 } else {
1102 circle_ctor.center = ctor.position;
1103 }
1104 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
1105 let mut circle_ctor = circle_ctor.clone();
1106 if circle.start == segment_id {
1107 circle_ctor.start = ctor.position;
1108 } else {
1109 circle_ctor.center = ctor.position;
1110 }
1111 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
1112 } else {
1113 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1114 "Internal: Circle does not have circle ctor, but found {}",
1115 circle.ctor.human_friendly_kind_with_article()
1116 ))));
1117 }
1118 continue;
1119 }
1120 Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
1121 let Some(control_index) =
1122 spline.controls.iter().position(|control_id| *control_id == segment_id)
1123 else {
1124 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1125 "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
1126 ))));
1127 };
1128 if let Some(existing) = final_edits.get_mut(&owner_id) {
1129 let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
1130 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1131 "Internal: Expected controlPointSpline ctor for owner, but found {}",
1132 existing.human_friendly_kind_with_article()
1133 ))));
1134 };
1135 spline_ctor.points[control_index] = ctor.position;
1136 } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
1137 let mut spline_ctor = spline_ctor.clone();
1138 spline_ctor.points[control_index] = ctor.position;
1139 final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
1140 } else {
1141 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1142 "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1143 spline.ctor.human_friendly_kind_with_article()
1144 ))));
1145 }
1146 continue;
1147 }
1148 _ => {}
1149 }
1150 }
1151
1152 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1154 }
1155 SegmentCtor::Line(ctor) => {
1156 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1157 }
1158 SegmentCtor::Arc(ctor) => {
1159 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1160 }
1161 SegmentCtor::Circle(ctor) => {
1162 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1163 }
1164 SegmentCtor::ControlPointSpline(ctor) => {
1165 final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1166 }
1167 }
1168 }
1169
1170 for (segment_id, ctor) in final_edits {
1171 match ctor {
1172 SegmentCtor::Point(ctor) => self
1173 .edit_point(&mut new_ast, sketch, segment_id, ctor)
1174 .map_err(KclErrorWithOutputs::no_outputs)?,
1175 SegmentCtor::Line(ctor) => self
1176 .edit_line(&mut new_ast, sketch, segment_id, ctor)
1177 .map_err(KclErrorWithOutputs::no_outputs)?,
1178 SegmentCtor::Arc(ctor) => self
1179 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1180 .map_err(KclErrorWithOutputs::no_outputs)?,
1181 SegmentCtor::Circle(ctor) => self
1182 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1183 .map_err(KclErrorWithOutputs::no_outputs)?,
1184 SegmentCtor::ControlPointSpline(ctor) => self
1185 .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1186 .map_err(KclErrorWithOutputs::no_outputs)?,
1187 }
1188 }
1189 for edit in constraint_label_edits {
1190 self.mutate_constraint_label_position(&mut new_ast, edit.constraint_id, edit.label_position)
1191 .map_err(KclErrorWithOutputs::no_outputs)?;
1192 }
1193 let (source_delta, mut scene_graph_delta) = self
1194 .execute_after_edit(
1195 ctx,
1196 sketch,
1197 sketch_block_ref,
1198 &mut new_ast,
1199 ExecuteAfterEditOptions {
1200 segment_ids_edited: drag_anchor_segment_ids,
1201 edit_kind: EditDeleteKind::Edit,
1202 commit_solved_initial_guesses,
1203 },
1204 )
1205 .await?;
1206 if invalidates_ids {
1207 scene_graph_delta.invalidates_ids = true;
1208 }
1209 Ok((source_delta, scene_graph_delta))
1210 }
1211
1212 async fn delete_objects(
1213 &mut self,
1214 ctx: &ExecutorContext,
1215 _version: Version,
1216 sketch: ObjectId,
1217 constraint_ids: Vec<ObjectId>,
1218 segment_ids: Vec<ObjectId>,
1219 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1220 let sketch_block_ref =
1222 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1223
1224 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1226 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1227
1228 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1231
1232 for segment_id in segment_ids_set.iter().copied() {
1233 let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1234 let ObjectKind::Segment { segment } = &segment_object.kind else {
1235 return None;
1236 };
1237 match segment {
1238 Segment::Point(point) => point.owner,
1239 Segment::Line(line) => line.owner,
1240 _ => None,
1241 }
1242 });
1243
1244 if let Some(owner_id) = owner_id
1245 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1246 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1247 && matches!(
1248 owner_segment,
1249 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1250 )
1251 {
1252 resolved_segment_ids_to_delete.insert(owner_id);
1254 } else {
1255 resolved_segment_ids_to_delete.insert(segment_id);
1257 }
1258 }
1259 let referenced_constraint_ids = self
1260 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1261 .map_err(KclErrorWithOutputs::no_outputs)?;
1262
1263 let mut new_ast = self.program.ast.clone();
1264
1265 for constraint_id in referenced_constraint_ids {
1266 if constraint_ids_set.contains(&constraint_id) {
1267 continue;
1268 }
1269
1270 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1271 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1272 })?;
1273 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1274 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1275 "Object is not a constraint, it is {}",
1276 constraint_object.kind.human_friendly_kind_with_article()
1277 ))));
1278 };
1279
1280 match constraint {
1281 Constraint::Coincident(coincident) => {
1282 let remaining_segments =
1283 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1284
1285 if remaining_segments.len() >= 2 {
1287 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1288 .map_err(KclErrorWithOutputs::no_outputs)?;
1289 } else {
1290 constraint_ids_set.insert(constraint_id);
1291 }
1292 }
1293 Constraint::EqualRadius(equal_radius) => {
1294 let remaining_input = equal_radius
1295 .input
1296 .iter()
1297 .copied()
1298 .filter(|segment_id| {
1299 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1300 })
1301 .collect::<Vec<_>>();
1302
1303 if remaining_input.len() >= 2 {
1304 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1305 .map_err(KclErrorWithOutputs::no_outputs)?;
1306 } else {
1307 constraint_ids_set.insert(constraint_id);
1308 }
1309 }
1310 Constraint::LinesEqualLength(lines_equal_length) => {
1311 let remaining_lines = lines_equal_length
1312 .lines
1313 .iter()
1314 .copied()
1315 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1316 .collect::<Vec<_>>();
1317
1318 if remaining_lines.len() >= 2 {
1320 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1321 .map_err(KclErrorWithOutputs::no_outputs)?;
1322 } else {
1323 constraint_ids_set.insert(constraint_id);
1324 }
1325 }
1326 Constraint::Parallel(parallel) => {
1327 let remaining_lines = parallel
1328 .lines
1329 .iter()
1330 .copied()
1331 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1332 .collect::<Vec<_>>();
1333
1334 if remaining_lines.len() >= 2 {
1335 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1336 .map_err(KclErrorWithOutputs::no_outputs)?;
1337 } else {
1338 constraint_ids_set.insert(constraint_id);
1339 }
1340 }
1341 Constraint::Horizontal(Horizontal::Points { points }) => {
1342 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1343
1344 if remaining_points.len() >= 2 {
1345 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1346 .map_err(KclErrorWithOutputs::no_outputs)?;
1347 } else {
1348 constraint_ids_set.insert(constraint_id);
1349 }
1350 }
1351 Constraint::Vertical(Vertical::Points { points }) => {
1352 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1353
1354 if remaining_points.len() >= 2 {
1355 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1356 .map_err(KclErrorWithOutputs::no_outputs)?;
1357 } else {
1358 constraint_ids_set.insert(constraint_id);
1359 }
1360 }
1361 Constraint::Fixed(fixed) => {
1362 if fixed.points.iter().any(|fixed_point| {
1363 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1364 }) {
1365 constraint_ids_set.insert(constraint_id);
1366 }
1367 }
1368 _ => {
1369 constraint_ids_set.insert(constraint_id);
1371 }
1372 }
1373 }
1374
1375 for constraint_id in constraint_ids_set {
1376 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1377 .map_err(KclErrorWithOutputs::no_outputs)?;
1378 }
1379 for segment_id in resolved_segment_ids_to_delete {
1380 self.delete_segment(&mut new_ast, sketch, segment_id)
1381 .map_err(KclErrorWithOutputs::no_outputs)?;
1382 }
1383
1384 self.execute_after_edit(
1385 ctx,
1386 sketch,
1387 sketch_block_ref,
1388 &mut new_ast,
1389 ExecuteAfterEditOptions {
1390 segment_ids_edited: Default::default(),
1391 edit_kind: EditDeleteKind::DeleteNonSketch,
1392 commit_solved_initial_guesses: true,
1393 },
1394 )
1395 .await
1396 }
1397
1398 async fn add_constraint(
1399 &mut self,
1400 ctx: &ExecutorContext,
1401 _version: Version,
1402 sketch: ObjectId,
1403 constraint: Constraint,
1404 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1405 let original_program = self.program.clone();
1409 let original_scene_graph = self.scene_graph.clone();
1410
1411 let mut new_ast = self.program.ast.clone();
1412 let sketch_block_ref = match constraint {
1413 Constraint::Coincident(coincident) => self
1414 .add_coincident(sketch, coincident, &mut new_ast)
1415 .await
1416 .map_err(KclErrorWithOutputs::no_outputs)?,
1417 Constraint::Distance(distance) => self
1418 .add_distance(sketch, distance, &mut new_ast)
1419 .await
1420 .map_err(KclErrorWithOutputs::no_outputs)?,
1421 Constraint::EqualRadius(equal_radius) => self
1422 .add_equal_radius(sketch, equal_radius, &mut new_ast)
1423 .await
1424 .map_err(KclErrorWithOutputs::no_outputs)?,
1425 Constraint::Fixed(fixed) => self
1426 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1427 .await
1428 .map_err(KclErrorWithOutputs::no_outputs)?,
1429 Constraint::HorizontalDistance(distance) => self
1430 .add_horizontal_distance(sketch, distance, &mut new_ast)
1431 .await
1432 .map_err(KclErrorWithOutputs::no_outputs)?,
1433 Constraint::VerticalDistance(distance) => self
1434 .add_vertical_distance(sketch, distance, &mut new_ast)
1435 .await
1436 .map_err(KclErrorWithOutputs::no_outputs)?,
1437 Constraint::Horizontal(horizontal) => self
1438 .add_horizontal(sketch, horizontal, &mut new_ast)
1439 .await
1440 .map_err(KclErrorWithOutputs::no_outputs)?,
1441 Constraint::LinesEqualLength(lines_equal_length) => self
1442 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1443 .await
1444 .map_err(KclErrorWithOutputs::no_outputs)?,
1445 Constraint::Midpoint(midpoint) => self
1446 .add_midpoint(sketch, midpoint, &mut new_ast)
1447 .await
1448 .map_err(KclErrorWithOutputs::no_outputs)?,
1449 Constraint::Parallel(parallel) => self
1450 .add_parallel(sketch, parallel, &mut new_ast)
1451 .await
1452 .map_err(KclErrorWithOutputs::no_outputs)?,
1453 Constraint::Perpendicular(perpendicular) => self
1454 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1455 .await
1456 .map_err(KclErrorWithOutputs::no_outputs)?,
1457 Constraint::Radius(radius) => self
1458 .add_radius(sketch, radius, &mut new_ast)
1459 .await
1460 .map_err(KclErrorWithOutputs::no_outputs)?,
1461 Constraint::Diameter(diameter) => self
1462 .add_diameter(sketch, diameter, &mut new_ast)
1463 .await
1464 .map_err(KclErrorWithOutputs::no_outputs)?,
1465 Constraint::Symmetric(symmetric) => self
1466 .add_symmetric(sketch, symmetric, &mut new_ast)
1467 .await
1468 .map_err(KclErrorWithOutputs::no_outputs)?,
1469 Constraint::Vertical(vertical) => self
1470 .add_vertical(sketch, vertical, &mut new_ast)
1471 .await
1472 .map_err(KclErrorWithOutputs::no_outputs)?,
1473 Constraint::Angle(lines_at_angle) => self
1474 .add_angle(sketch, lines_at_angle, &mut new_ast)
1475 .await
1476 .map_err(KclErrorWithOutputs::no_outputs)?,
1477 Constraint::Tangent(tangent) => self
1478 .add_tangent(sketch, tangent, &mut new_ast)
1479 .await
1480 .map_err(KclErrorWithOutputs::no_outputs)?,
1481 };
1482
1483 let result = self
1484 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1485 .await;
1486
1487 if result.is_err() {
1489 self.program = original_program;
1490 self.scene_graph = original_scene_graph;
1491 }
1492
1493 result
1494 }
1495
1496 async fn chain_segment(
1497 &mut self,
1498 ctx: &ExecutorContext,
1499 version: Version,
1500 sketch: ObjectId,
1501 previous_segment_end_point_id: ObjectId,
1502 segment: SegmentCtor,
1503 _label: Option<String>,
1504 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1505 let SegmentCtor::Line(line_ctor) = segment else {
1509 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1510 "chain_segment currently only supports Line segments, got {}",
1511 segment.human_friendly_kind_with_article(),
1512 ))));
1513 };
1514
1515 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1517
1518 let new_line_id = first_scene_delta
1521 .new_objects
1522 .iter()
1523 .find(|&obj_id| {
1524 let obj = self.scene_graph.objects.get(obj_id.0);
1525 if let Some(obj) = obj {
1526 matches!(
1527 &obj.kind,
1528 ObjectKind::Segment {
1529 segment: Segment::Line(_)
1530 }
1531 )
1532 } else {
1533 false
1534 }
1535 })
1536 .ok_or_else(|| {
1537 KclErrorWithOutputs::no_outputs(KclError::refactor(
1538 "Failed to find new line segment in scene graph".to_string(),
1539 ))
1540 })?;
1541
1542 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1543 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1544 "New line object not found: {new_line_id:?}"
1545 )))
1546 })?;
1547
1548 let ObjectKind::Segment {
1549 segment: new_line_segment,
1550 } = &new_line_obj.kind
1551 else {
1552 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1553 "Object is not a segment: {new_line_obj:?}"
1554 ))));
1555 };
1556
1557 let Segment::Line(new_line) = new_line_segment else {
1558 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1559 "Segment is not a line: {new_line_segment:?}"
1560 ))));
1561 };
1562
1563 let new_line_start_point_id = new_line.start;
1564
1565 let coincident = Coincident {
1567 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1568 };
1569
1570 let (final_src_delta, final_scene_delta) = self
1571 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1572 .await?;
1573
1574 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1577 combined_new_objects.extend(final_scene_delta.new_objects);
1578
1579 let scene_graph_delta = SceneGraphDelta {
1580 new_graph: self.scene_graph_for_ui(),
1581 invalidates_ids: false,
1582 new_objects: combined_new_objects,
1583 exec_outcome: final_scene_delta.exec_outcome,
1584 };
1585
1586 Ok((final_src_delta, scene_graph_delta))
1587 }
1588
1589 async fn edit_constraint_value(
1591 &mut self,
1592 ctx: &ExecutorContext,
1593 _version: Version,
1594 sketch: ObjectId,
1595 constraint_id: ObjectId,
1596 value_expression: String,
1597 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1598 let sketch_block_ref =
1600 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1601
1602 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1603 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1604 })?;
1605 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1606 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1607 "Object is not a constraint: {constraint_id:?}"
1608 ))));
1609 }
1610
1611 let mut new_ast = self.program.ast.clone();
1612
1613 let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1615 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1616 "Invalid constraint value",
1617 &e,
1618 )))
1619 })?;
1620 if !errors.is_empty() {
1621 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1622 format_compilation_issues("Invalid constraint value", &errors),
1623 )));
1624 }
1625 let mut parsed = parsed.ok_or_else(|| {
1626 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1627 })?;
1628 if parsed.ast.body.is_empty() {
1629 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1630 "Empty value expression".to_string(),
1631 )));
1632 }
1633 let first = parsed.ast.body.remove(0);
1634 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1635 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1636 "Value expression must be a simple expression".to_string(),
1637 )));
1638 };
1639
1640 let new_value: ast::BinaryPart = expr_stmt
1641 .inner
1642 .expression
1643 .try_into()
1644 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1645
1646 self.mutate_ast(
1647 &mut new_ast,
1648 constraint_id,
1649 AstMutateCommand::EditConstraintValue { value: new_value },
1650 )
1651 .map_err(KclErrorWithOutputs::no_outputs)?;
1652
1653 self.execute_after_edit(
1654 ctx,
1655 sketch,
1656 sketch_block_ref,
1657 &mut new_ast,
1658 ExecuteAfterEditOptions {
1659 segment_ids_edited: Default::default(),
1660 edit_kind: EditDeleteKind::Edit,
1661 commit_solved_initial_guesses: true,
1662 },
1663 )
1664 .await
1665 }
1666
1667 async fn edit_distance_constraint_label_position(
1668 &mut self,
1669 ctx: &ExecutorContext,
1670 _version: Version,
1671 sketch: ObjectId,
1672 constraint_id: ObjectId,
1673 label_position: Point2d<Number>,
1674 anchor_segment_ids: Vec<ObjectId>,
1675 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1676 let sketch_block_ref =
1678 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1679
1680 let mut new_ast = self.program.ast.clone();
1681 self.mutate_constraint_label_position(&mut new_ast, constraint_id, label_position)
1682 .map_err(KclErrorWithOutputs::no_outputs)?;
1683 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1684
1685 self.execute_after_edit(
1686 ctx,
1687 sketch,
1688 sketch_block_ref,
1689 &mut new_ast,
1690 ExecuteAfterEditOptions {
1691 segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1692 edit_kind: EditDeleteKind::Edit,
1693 commit_solved_initial_guesses,
1694 },
1695 )
1696 .await
1697 }
1698
1699 async fn batch_split_segment_operations(
1707 &mut self,
1708 ctx: &ExecutorContext,
1709 _version: Version,
1710 sketch: ObjectId,
1711 edit_segments: Vec<ExistingSegmentCtor>,
1712 add_constraints: Vec<Constraint>,
1713 delete_constraint_ids: Vec<ObjectId>,
1714 _new_segment_info: sketch::NewSegmentInfo,
1715 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1716 let sketch_block_ref =
1718 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1719
1720 let mut new_ast = self.program.ast.clone();
1721 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1722
1723 for segment in edit_segments {
1725 segment_ids_edited.insert(segment.id);
1726 match segment.ctor {
1727 SegmentCtor::Point(ctor) => self
1728 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1729 .map_err(KclErrorWithOutputs::no_outputs)?,
1730 SegmentCtor::Line(ctor) => self
1731 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1732 .map_err(KclErrorWithOutputs::no_outputs)?,
1733 SegmentCtor::Arc(ctor) => self
1734 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1735 .map_err(KclErrorWithOutputs::no_outputs)?,
1736 SegmentCtor::Circle(ctor) => self
1737 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1738 .map_err(KclErrorWithOutputs::no_outputs)?,
1739 SegmentCtor::ControlPointSpline(ctor) => self
1740 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1741 .map_err(KclErrorWithOutputs::no_outputs)?,
1742 }
1743 }
1744
1745 for constraint in add_constraints {
1747 match constraint {
1748 Constraint::Coincident(coincident) => {
1749 self.add_coincident(sketch, coincident, &mut new_ast)
1750 .await
1751 .map_err(KclErrorWithOutputs::no_outputs)?;
1752 }
1753 Constraint::Distance(distance) => {
1754 self.add_distance(sketch, distance, &mut new_ast)
1755 .await
1756 .map_err(KclErrorWithOutputs::no_outputs)?;
1757 }
1758 Constraint::EqualRadius(equal_radius) => {
1759 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1760 .await
1761 .map_err(KclErrorWithOutputs::no_outputs)?;
1762 }
1763 Constraint::Fixed(fixed) => {
1764 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1765 .await
1766 .map_err(KclErrorWithOutputs::no_outputs)?;
1767 }
1768 Constraint::HorizontalDistance(distance) => {
1769 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1770 .await
1771 .map_err(KclErrorWithOutputs::no_outputs)?;
1772 }
1773 Constraint::VerticalDistance(distance) => {
1774 self.add_vertical_distance(sketch, distance, &mut new_ast)
1775 .await
1776 .map_err(KclErrorWithOutputs::no_outputs)?;
1777 }
1778 Constraint::Horizontal(horizontal) => {
1779 self.add_horizontal(sketch, horizontal, &mut new_ast)
1780 .await
1781 .map_err(KclErrorWithOutputs::no_outputs)?;
1782 }
1783 Constraint::LinesEqualLength(lines_equal_length) => {
1784 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1785 .await
1786 .map_err(KclErrorWithOutputs::no_outputs)?;
1787 }
1788 Constraint::Midpoint(midpoint) => {
1789 self.add_midpoint(sketch, midpoint, &mut new_ast)
1790 .await
1791 .map_err(KclErrorWithOutputs::no_outputs)?;
1792 }
1793 Constraint::Parallel(parallel) => {
1794 self.add_parallel(sketch, parallel, &mut new_ast)
1795 .await
1796 .map_err(KclErrorWithOutputs::no_outputs)?;
1797 }
1798 Constraint::Perpendicular(perpendicular) => {
1799 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1800 .await
1801 .map_err(KclErrorWithOutputs::no_outputs)?;
1802 }
1803 Constraint::Vertical(vertical) => {
1804 self.add_vertical(sketch, vertical, &mut new_ast)
1805 .await
1806 .map_err(KclErrorWithOutputs::no_outputs)?;
1807 }
1808 Constraint::Diameter(diameter) => {
1809 self.add_diameter(sketch, diameter, &mut new_ast)
1810 .await
1811 .map_err(KclErrorWithOutputs::no_outputs)?;
1812 }
1813 Constraint::Radius(radius) => {
1814 self.add_radius(sketch, radius, &mut new_ast)
1815 .await
1816 .map_err(KclErrorWithOutputs::no_outputs)?;
1817 }
1818 Constraint::Symmetric(symmetric) => {
1819 self.add_symmetric(sketch, symmetric, &mut new_ast)
1820 .await
1821 .map_err(KclErrorWithOutputs::no_outputs)?;
1822 }
1823 Constraint::Angle(angle) => {
1824 self.add_angle(sketch, angle, &mut new_ast)
1825 .await
1826 .map_err(KclErrorWithOutputs::no_outputs)?;
1827 }
1828 Constraint::Tangent(tangent) => {
1829 self.add_tangent(sketch, tangent, &mut new_ast)
1830 .await
1831 .map_err(KclErrorWithOutputs::no_outputs)?;
1832 }
1833 }
1834 }
1835
1836 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1838
1839 let has_constraint_deletions = !constraint_ids_set.is_empty();
1840 for constraint_id in constraint_ids_set {
1841 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1842 .map_err(KclErrorWithOutputs::no_outputs)?;
1843 }
1844
1845 let (source_delta, mut scene_graph_delta) = self
1849 .execute_after_edit(
1850 ctx,
1851 sketch,
1852 sketch_block_ref,
1853 &mut new_ast,
1854 ExecuteAfterEditOptions {
1855 segment_ids_edited,
1856 edit_kind: EditDeleteKind::Edit,
1857 commit_solved_initial_guesses: true,
1858 },
1859 )
1860 .await?;
1861
1862 if has_constraint_deletions {
1865 scene_graph_delta.invalidates_ids = true;
1866 }
1867
1868 Ok((source_delta, scene_graph_delta))
1869 }
1870
1871 async fn batch_tail_cut_operations(
1872 &mut self,
1873 ctx: &ExecutorContext,
1874 _version: Version,
1875 sketch: ObjectId,
1876 edit_segments: Vec<ExistingSegmentCtor>,
1877 add_constraints: Vec<Constraint>,
1878 delete_constraint_ids: Vec<ObjectId>,
1879 additional_edited_segment_ids: Vec<ObjectId>,
1880 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1881 let sketch_block_ref =
1882 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1883
1884 let mut new_ast = self.program.ast.clone();
1885 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1886
1887 for segment in edit_segments {
1889 segment_ids_edited.insert(segment.id);
1890 match segment.ctor {
1891 SegmentCtor::Point(ctor) => self
1892 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1893 .map_err(KclErrorWithOutputs::no_outputs)?,
1894 SegmentCtor::Line(ctor) => self
1895 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1896 .map_err(KclErrorWithOutputs::no_outputs)?,
1897 SegmentCtor::Arc(ctor) => self
1898 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1899 .map_err(KclErrorWithOutputs::no_outputs)?,
1900 SegmentCtor::Circle(ctor) => self
1901 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1902 .map_err(KclErrorWithOutputs::no_outputs)?,
1903 SegmentCtor::ControlPointSpline(ctor) => self
1904 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1905 .map_err(KclErrorWithOutputs::no_outputs)?,
1906 }
1907 }
1908
1909 segment_ids_edited.extend(additional_edited_segment_ids);
1910
1911 for constraint in add_constraints {
1913 match constraint {
1914 Constraint::Coincident(coincident) => {
1915 self.add_coincident(sketch, coincident, &mut new_ast)
1916 .await
1917 .map_err(KclErrorWithOutputs::no_outputs)?;
1918 }
1919 other => {
1920 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1921 "unsupported constraint in tail cut batch: {other:?}"
1922 ))));
1923 }
1924 }
1925 }
1926
1927 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1929
1930 let has_constraint_deletions = !constraint_ids_set.is_empty();
1931 for constraint_id in constraint_ids_set {
1932 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1933 .map_err(KclErrorWithOutputs::no_outputs)?;
1934 }
1935
1936 let (source_delta, mut scene_graph_delta) = self
1940 .execute_after_edit(
1941 ctx,
1942 sketch,
1943 sketch_block_ref,
1944 &mut new_ast,
1945 ExecuteAfterEditOptions {
1946 segment_ids_edited,
1947 edit_kind: EditDeleteKind::Edit,
1948 commit_solved_initial_guesses: true,
1949 },
1950 )
1951 .await?;
1952
1953 if has_constraint_deletions {
1956 scene_graph_delta.invalidates_ids = true;
1957 }
1958
1959 Ok((source_delta, scene_graph_delta))
1960 }
1961}
1962
1963impl FrontendState {
1964 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1965 self.program = program.clone();
1966
1967 self.point_freedom_cache.clear();
1978 match ctx.run_with_caching(program).await {
1979 Ok(outcome) => {
1980 let outcome = self.update_state_after_exec(outcome, true);
1981 let checkpoint_id = self
1982 .create_sketch_checkpoint(outcome.clone())
1983 .await
1984 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1985 Ok(SetProgramOutcome::Success {
1986 scene_graph: Box::new(self.scene_graph_for_ui()),
1987 exec_outcome: Box::new(outcome),
1988 checkpoint_id: Some(checkpoint_id),
1989 })
1990 }
1991 Err(mut err) => {
1992 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1995 self.update_state_after_exec(outcome, true);
1996 err.scene_graph = Some(self.scene_graph_for_ui());
1997 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1998 }
1999 }
2000 }
2001
2002 pub async fn engine_execute(
2005 &mut self,
2006 ctx: &ExecutorContext,
2007 program: Program,
2008 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
2009 self.program = program.clone();
2010
2011 self.point_freedom_cache.clear();
2015 match ctx.run_with_caching(program).await {
2016 Ok(outcome) => {
2017 let outcome = self.update_state_after_exec(outcome, true);
2018 Ok(SceneGraphDelta {
2019 new_graph: self.scene_graph_for_ui(),
2020 exec_outcome: outcome,
2021 new_objects: Default::default(),
2023 invalidates_ids: Default::default(),
2025 })
2026 }
2027 Err(mut err) => {
2028 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2030 self.update_state_after_exec(outcome, true);
2031 err.scene_graph = Some(self.scene_graph_for_ui());
2032 Err(err)
2033 }
2034 }
2035 }
2036
2037 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
2038 if matches!(err.error, KclError::EngineHangup { .. }) {
2039 return Err(err);
2043 }
2044
2045 let KclErrorWithOutputs {
2046 error,
2047 mut non_fatal,
2048 variables,
2049 operations,
2050 artifact_graph,
2051 scene_objects,
2052 source_range_to_object,
2053 var_solutions,
2054 refactor_metadata,
2055 filenames,
2056 source_files,
2057 default_planes,
2058 ..
2059 } = err;
2060
2061 non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2062
2063 Ok(ExecOutcome {
2064 variables,
2065 filenames,
2066 operations,
2067 artifact_graph,
2068 scene_objects,
2069 source_range_to_object,
2070 var_solutions,
2071 refactor_metadata,
2072 issues: non_fatal,
2073 source_files,
2074 default_planes,
2075 })
2076 }
2077
2078 async fn add_point(
2079 &mut self,
2080 ctx: &ExecutorContext,
2081 sketch: ObjectId,
2082 ctor: PointCtor,
2083 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2084 let at_ast = to_ast_point2d(&ctor.position)
2086 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2087 let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2088 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2089 unlabeled: None,
2090 arguments: vec![ast::LabeledArg {
2091 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2092 arg: at_ast,
2093 }],
2094 digest: None,
2095 non_code_meta: Default::default(),
2096 })));
2097
2098 let sketch_id = sketch;
2100 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2101 #[cfg(target_arch = "wasm32")]
2102 web_sys::console::error_1(
2103 &format!(
2104 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2105 self.scene_graph.objects
2106 )
2107 .into(),
2108 );
2109 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2110 })?;
2111 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2112 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2113 "Object is not a sketch, it is {}",
2114 sketch_object.kind.human_friendly_kind_with_article(),
2115 ))));
2116 };
2117 let mut new_ast = self.program.ast.clone();
2119 let (sketch_block_ref, _) = self
2120 .mutate_ast(
2121 &mut new_ast,
2122 sketch_id,
2123 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2124 )
2125 .map_err(KclErrorWithOutputs::no_outputs)?;
2126 let new_source = source_from_ast(&new_ast);
2128 let new_program = parse_frontend_mutation_source(
2130 &new_source,
2131 "Error parsing KCL source after adding point",
2132 "No AST produced after adding point",
2133 )?;
2134
2135 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2136 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2137 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2138 )))
2139 })?;
2140
2141 self.program = new_program.clone();
2143
2144 let mut truncated_program = new_program;
2146 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2147 .map_err(KclErrorWithOutputs::no_outputs)?;
2148
2149 let outcome = ctx
2151 .run_mock(
2152 &truncated_program,
2153 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2154 )
2155 .await?;
2156
2157 let new_object_ids = {
2158 let make_err =
2159 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2160 let segment_id = outcome
2161 .source_range_to_object
2162 .get(&point_node_ref.range)
2163 .copied()
2164 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2165 let segment_object = outcome
2166 .scene_objects
2167 .get(segment_id.0)
2168 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2169 let ObjectKind::Segment { segment } = &segment_object.kind else {
2170 return Err(make_err(format!(
2171 "Object is not a segment, it is {}",
2172 segment_object.kind.human_friendly_kind_with_article()
2173 )));
2174 };
2175 let Segment::Point(_) = segment else {
2176 return Err(make_err(format!(
2177 "Segment is not a point, it is {}",
2178 segment.human_friendly_kind_with_article()
2179 )));
2180 };
2181 vec![segment_id]
2182 };
2183 let src_delta = SourceDelta { text: new_source };
2184 let outcome = self.update_state_after_exec(outcome, false);
2186 let scene_graph_delta = SceneGraphDelta {
2187 new_graph: self.scene_graph_for_ui(),
2188 invalidates_ids: false,
2189 new_objects: new_object_ids,
2190 exec_outcome: outcome,
2191 };
2192 Ok((src_delta, scene_graph_delta))
2193 }
2194
2195 async fn add_line(
2196 &mut self,
2197 ctx: &ExecutorContext,
2198 sketch: ObjectId,
2199 ctor: LineCtor,
2200 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2201 let start_ast = to_ast_point2d(&ctor.start)
2203 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2204 let end_ast = to_ast_point2d(&ctor.end)
2205 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2206 let mut arguments = vec![
2207 ast::LabeledArg {
2208 label: Some(ast::Identifier::new(LINE_START_PARAM)),
2209 arg: start_ast,
2210 },
2211 ast::LabeledArg {
2212 label: Some(ast::Identifier::new(LINE_END_PARAM)),
2213 arg: end_ast,
2214 },
2215 ];
2216 if ctor.construction == Some(true) {
2218 arguments.push(ast::LabeledArg {
2219 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2220 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2221 value: ast::LiteralValue::Bool(true),
2222 raw: "true".to_string(),
2223 digest: None,
2224 }))),
2225 });
2226 }
2227 let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2228 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2229 unlabeled: None,
2230 arguments,
2231 digest: None,
2232 non_code_meta: Default::default(),
2233 })));
2234
2235 let sketch_id = sketch;
2237 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2238 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2239 })?;
2240 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2241 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2242 "Object is not a sketch, it is {}",
2243 sketch_object.kind.human_friendly_kind_with_article(),
2244 ))));
2245 };
2246 let mut new_ast = self.program.ast.clone();
2248 let (sketch_block_ref, _) = self
2249 .mutate_ast(
2250 &mut new_ast,
2251 sketch_id,
2252 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2253 )
2254 .map_err(KclErrorWithOutputs::no_outputs)?;
2255 let new_source = source_from_ast(&new_ast);
2257 let new_program = parse_frontend_mutation_source(
2259 &new_source,
2260 "Error parsing KCL source after adding line",
2261 "No AST produced after adding line",
2262 )?;
2263
2264 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2265 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2266 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2267 )))
2268 })?;
2269
2270 self.program = new_program.clone();
2272
2273 let mut truncated_program = new_program;
2275 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2276 .map_err(KclErrorWithOutputs::no_outputs)?;
2277
2278 let outcome = ctx
2280 .run_mock(
2281 &truncated_program,
2282 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2283 )
2284 .await?;
2285
2286 let new_object_ids = {
2287 let make_err =
2288 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2289 let segment_id = outcome
2290 .source_range_to_object
2291 .get(&line_node_ref.range)
2292 .copied()
2293 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2294 let segment_object = outcome
2295 .scene_object_by_id(segment_id)
2296 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2297 let ObjectKind::Segment { segment } = &segment_object.kind else {
2298 return Err(make_err(format!(
2299 "Object is not a segment, it is {}",
2300 segment_object.kind.human_friendly_kind_with_article()
2301 )));
2302 };
2303 let Segment::Line(line) = segment else {
2304 return Err(make_err(format!(
2305 "Segment is not a line, it is {}",
2306 segment.human_friendly_kind_with_article()
2307 )));
2308 };
2309 vec![line.start, line.end, segment_id]
2310 };
2311 let src_delta = SourceDelta { text: new_source };
2312 let outcome = self.update_state_after_exec(outcome, false);
2314 let scene_graph_delta = SceneGraphDelta {
2315 new_graph: self.scene_graph_for_ui(),
2316 invalidates_ids: false,
2317 new_objects: new_object_ids,
2318 exec_outcome: outcome,
2319 };
2320 Ok((src_delta, scene_graph_delta))
2321 }
2322
2323 async fn add_arc(
2324 &mut self,
2325 ctx: &ExecutorContext,
2326 sketch: ObjectId,
2327 ctor: ArcCtor,
2328 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2329 let start_ast = to_ast_point2d(&ctor.start)
2331 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2332 let end_ast = to_ast_point2d(&ctor.end)
2333 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2334 let center_ast = to_ast_point2d(&ctor.center)
2335 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2336 let mut arguments = vec![
2337 ast::LabeledArg {
2338 label: Some(ast::Identifier::new(ARC_START_PARAM)),
2339 arg: start_ast,
2340 },
2341 ast::LabeledArg {
2342 label: Some(ast::Identifier::new(ARC_END_PARAM)),
2343 arg: end_ast,
2344 },
2345 ast::LabeledArg {
2346 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2347 arg: center_ast,
2348 },
2349 ];
2350 if ctor.direction == Some(ArcDirection::Cw) {
2353 arguments.push(ast::LabeledArg {
2354 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2355 arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2356 });
2357 }
2358 if ctor.construction == Some(true) {
2360 arguments.push(ast::LabeledArg {
2361 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2362 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2363 value: ast::LiteralValue::Bool(true),
2364 raw: "true".to_string(),
2365 digest: None,
2366 }))),
2367 });
2368 }
2369 let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2370 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2371 unlabeled: None,
2372 arguments,
2373 digest: None,
2374 non_code_meta: Default::default(),
2375 })));
2376
2377 let sketch_id = sketch;
2379 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2380 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2381 })?;
2382 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2383 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2384 "Object is not a sketch, it is {}",
2385 sketch_object.kind.human_friendly_kind_with_article(),
2386 ))));
2387 };
2388 let mut new_ast = self.program.ast.clone();
2390 let (sketch_block_ref, _) = self
2391 .mutate_ast(
2392 &mut new_ast,
2393 sketch_id,
2394 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2395 )
2396 .map_err(KclErrorWithOutputs::no_outputs)?;
2397 let new_source = source_from_ast(&new_ast);
2399 let new_program = parse_frontend_mutation_source(
2401 &new_source,
2402 "Error parsing KCL source after adding arc",
2403 "No AST produced after adding arc",
2404 )?;
2405
2406 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2407 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2408 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2409 )))
2410 })?;
2411
2412 self.program = new_program.clone();
2414
2415 let mut truncated_program = new_program;
2417 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2418 .map_err(KclErrorWithOutputs::no_outputs)?;
2419
2420 let outcome = ctx
2422 .run_mock(
2423 &truncated_program,
2424 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2425 )
2426 .await?;
2427
2428 let new_object_ids = {
2429 let make_err =
2430 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2431 let segment_id = outcome
2432 .source_range_to_object
2433 .get(&arc_node_ref.range)
2434 .copied()
2435 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2436 let segment_object = outcome
2437 .scene_objects
2438 .get(segment_id.0)
2439 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2440 let ObjectKind::Segment { segment } = &segment_object.kind else {
2441 return Err(make_err(format!(
2442 "Object is not a segment, it is {}",
2443 segment_object.kind.human_friendly_kind_with_article()
2444 )));
2445 };
2446 let Segment::Arc(arc) = segment else {
2447 return Err(make_err(format!(
2448 "Segment is not an arc, it is {}",
2449 segment.human_friendly_kind_with_article()
2450 )));
2451 };
2452 vec![arc.start, arc.end, arc.center, segment_id]
2453 };
2454 let src_delta = SourceDelta { text: new_source };
2455 let outcome = self.update_state_after_exec(outcome, false);
2457 let scene_graph_delta = SceneGraphDelta {
2458 new_graph: self.scene_graph_for_ui(),
2459 invalidates_ids: false,
2460 new_objects: new_object_ids,
2461 exec_outcome: outcome,
2462 };
2463 Ok((src_delta, scene_graph_delta))
2464 }
2465
2466 async fn add_circle(
2467 &mut self,
2468 ctx: &ExecutorContext,
2469 sketch: ObjectId,
2470 ctor: CircleCtor,
2471 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2472 let start_ast = to_ast_point2d(&ctor.start)
2474 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2475 let center_ast = to_ast_point2d(&ctor.center)
2476 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2477 let mut arguments = vec![
2478 ast::LabeledArg {
2479 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2480 arg: start_ast,
2481 },
2482 ast::LabeledArg {
2483 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2484 arg: center_ast,
2485 },
2486 ];
2487 if ctor.construction == Some(true) {
2489 arguments.push(ast::LabeledArg {
2490 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2491 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2492 value: ast::LiteralValue::Bool(true),
2493 raw: "true".to_string(),
2494 digest: None,
2495 }))),
2496 });
2497 }
2498 let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2499 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2500 unlabeled: None,
2501 arguments,
2502 digest: None,
2503 non_code_meta: Default::default(),
2504 })));
2505
2506 let sketch_id = sketch;
2508 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2509 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2510 })?;
2511 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2512 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2513 "Object is not a sketch, it is {}",
2514 sketch_object.kind.human_friendly_kind_with_article(),
2515 ))));
2516 };
2517 let mut new_ast = self.program.ast.clone();
2519 let (sketch_block_ref, _) = self
2520 .mutate_ast(
2521 &mut new_ast,
2522 sketch_id,
2523 AstMutateCommand::AddSketchBlockVarDecl {
2524 prefix: CIRCLE_VARIABLE.to_owned(),
2525 expr: circle_ast,
2526 },
2527 )
2528 .map_err(KclErrorWithOutputs::no_outputs)?;
2529 let new_source = source_from_ast(&new_ast);
2531 let new_program = parse_frontend_mutation_source(
2533 &new_source,
2534 "Error parsing KCL source after adding circle",
2535 "No AST produced after adding circle",
2536 )?;
2537
2538 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2539 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2540 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2541 )))
2542 })?;
2543
2544 self.program = new_program.clone();
2546
2547 let mut truncated_program = new_program;
2549 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2550 .map_err(KclErrorWithOutputs::no_outputs)?;
2551
2552 let outcome = ctx
2554 .run_mock(
2555 &truncated_program,
2556 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2557 )
2558 .await?;
2559
2560 let new_object_ids = {
2561 let make_err =
2562 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2563 let segment_id = outcome
2564 .source_range_to_object
2565 .get(&circle_node_ref.range)
2566 .copied()
2567 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2568 let segment_object = outcome
2569 .scene_objects
2570 .get(segment_id.0)
2571 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2572 let ObjectKind::Segment { segment } = &segment_object.kind else {
2573 return Err(make_err(format!(
2574 "Object is not a segment, it is {}",
2575 segment_object.kind.human_friendly_kind_with_article()
2576 )));
2577 };
2578 let Segment::Circle(circle) = segment else {
2579 return Err(make_err(format!(
2580 "Segment is not a circle, it is {}",
2581 segment.human_friendly_kind_with_article()
2582 )));
2583 };
2584 vec![circle.start, circle.center, segment_id]
2585 };
2586 let src_delta = SourceDelta { text: new_source };
2587 let outcome = self.update_state_after_exec(outcome, false);
2589 let scene_graph_delta = SceneGraphDelta {
2590 new_graph: self.scene_graph_for_ui(),
2591 invalidates_ids: false,
2592 new_objects: new_object_ids,
2593 exec_outcome: outcome,
2594 };
2595 Ok((src_delta, scene_graph_delta))
2596 }
2597
2598 async fn add_control_point_spline(
2599 &mut self,
2600 ctx: &ExecutorContext,
2601 sketch: ObjectId,
2602 ctor: ControlPointSplineCtor,
2603 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2604 let new_program = ensure_control_point_spline_experimental_features(&self.program)
2605 .map_err(KclErrorWithOutputs::no_outputs)?;
2606
2607 let points_ast = to_ast_point2d_array(&ctor.points)
2608 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2609 let mut arguments = vec![ast::LabeledArg {
2610 label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2611 arg: points_ast,
2612 }];
2613 if ctor.construction == Some(true) {
2614 arguments.push(ast::LabeledArg {
2615 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2616 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2617 value: ast::LiteralValue::Bool(true),
2618 raw: "true".to_string(),
2619 digest: None,
2620 }))),
2621 });
2622 }
2623 let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2624 callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2625 unlabeled: None,
2626 arguments,
2627 digest: None,
2628 non_code_meta: Default::default(),
2629 })));
2630
2631 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2632 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2633 })?;
2634 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2635 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2636 "Object is not a sketch, it is {}",
2637 sketch_object.kind.human_friendly_kind_with_article(),
2638 ))));
2639 };
2640
2641 let mut new_ast = new_program.ast.clone();
2642 let (sketch_block_ref, _) = self
2643 .mutate_ast(
2644 &mut new_ast,
2645 sketch,
2646 AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2647 )
2648 .map_err(KclErrorWithOutputs::no_outputs)?;
2649 let new_source = source_from_ast(&new_ast);
2650 let new_program = parse_frontend_mutation_source(
2651 &new_source,
2652 "Error parsing KCL source after adding controlPointSpline",
2653 "No AST produced after adding controlPointSpline",
2654 )?;
2655
2656 let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2657 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2658 "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2659 )))
2660 })?;
2661
2662 self.program = new_program.clone();
2663
2664 let mut truncated_program = new_program;
2665 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2666 .map_err(KclErrorWithOutputs::no_outputs)?;
2667
2668 let outcome = ctx
2669 .run_mock(
2670 &truncated_program,
2671 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2672 )
2673 .await?;
2674
2675 let new_object_ids = {
2676 let make_err =
2677 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2678 let segment_id = outcome
2679 .source_range_to_object
2680 .get(&spline_node_ref.range)
2681 .copied()
2682 .ok_or_else(|| {
2683 make_err(format!(
2684 "Source range of controlPointSpline not found: {spline_node_ref:?}"
2685 ))
2686 })?;
2687 let segment_object = outcome
2688 .scene_objects
2689 .get(segment_id.0)
2690 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2691 let ObjectKind::Segment { segment } = &segment_object.kind else {
2692 return Err(make_err(format!(
2693 "Object is not a segment, it is {}",
2694 segment_object.kind.human_friendly_kind_with_article()
2695 )));
2696 };
2697 let Segment::ControlPointSpline(spline) = segment else {
2698 return Err(make_err(format!(
2699 "Segment is not a control point spline, it is {}",
2700 segment.human_friendly_kind_with_article()
2701 )));
2702 };
2703
2704 let mut ids = outcome
2705 .scene_objects
2706 .iter()
2707 .filter_map(|obj| match &obj.kind {
2708 ObjectKind::Segment {
2709 segment: Segment::Line(line),
2710 } if line.owner == Some(segment_id) => Some(obj.id),
2711 _ => None,
2712 })
2713 .collect::<Vec<_>>();
2714 ids.extend(spline.controls.clone());
2715 ids.push(segment_id);
2716 ids
2717 };
2718 let src_delta = SourceDelta { text: new_source };
2719 let outcome = self.update_state_after_exec(outcome, false);
2720 let scene_graph_delta = SceneGraphDelta {
2721 new_graph: self.scene_graph_for_ui(),
2722 invalidates_ids: false,
2723 new_objects: new_object_ids,
2724 exec_outcome: outcome,
2725 };
2726 Ok((src_delta, scene_graph_delta))
2727 }
2728
2729 fn edit_point(
2730 &mut self,
2731 new_ast: &mut ast::Node<ast::Program>,
2732 sketch: ObjectId,
2733 point: ObjectId,
2734 ctor: PointCtor,
2735 ) -> Result<(), KclError> {
2736 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2738
2739 let sketch_id = sketch;
2741 let sketch_object = self
2742 .scene_graph
2743 .objects
2744 .get(sketch_id.0)
2745 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2746 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2747 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2748 };
2749 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2750 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2751 })?;
2752 let point_id = point;
2754 let point_object = self
2755 .scene_graph
2756 .objects
2757 .get(point_id.0)
2758 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2759 let ObjectKind::Segment {
2760 segment: Segment::Point(point),
2761 } = &point_object.kind
2762 else {
2763 return Err(KclError::refactor(format!(
2764 "Object is not a point segment: {point_object:?}"
2765 )));
2766 };
2767
2768 if let Some(owner_id) = point.owner {
2770 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2771 KclError::refactor(format!(
2772 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2773 ))
2774 })?;
2775 let ObjectKind::Segment { segment } = &owner_object.kind else {
2776 return Err(KclError::refactor(format!(
2777 "Internal: Owner of point is not a segment, but found {}",
2778 owner_object.kind.human_friendly_kind_with_article()
2779 )));
2780 };
2781
2782 if let Segment::Line(line) = segment {
2784 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2785 return Err(KclError::refactor(format!(
2786 "Internal: Owner of point does not have line ctor, but found {}",
2787 line.ctor.human_friendly_kind_with_article()
2788 )));
2789 };
2790 let mut line_ctor = line_ctor.clone();
2791 if line.start == point_id {
2793 line_ctor.start = ctor.position;
2794 } else if line.end == point_id {
2795 line_ctor.end = ctor.position;
2796 } else {
2797 return Err(KclError::refactor(format!(
2798 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2799 )));
2800 }
2801 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2802 }
2803
2804 if let Segment::Arc(arc) = segment {
2806 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2807 return Err(KclError::refactor(format!(
2808 "Internal: Owner of point does not have arc ctor, but found {}",
2809 arc.ctor.human_friendly_kind_with_article()
2810 )));
2811 };
2812 let mut arc_ctor = arc_ctor.clone();
2813 if arc.center == point_id {
2815 arc_ctor.center = ctor.position;
2816 } else if arc.start == point_id {
2817 arc_ctor.start = ctor.position;
2818 } else if arc.end == point_id {
2819 arc_ctor.end = ctor.position;
2820 } else {
2821 return Err(KclError::refactor(format!(
2822 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2823 )));
2824 }
2825 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2826 }
2827
2828 if let Segment::Circle(circle) = segment {
2830 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2831 return Err(KclError::refactor(format!(
2832 "Internal: Owner of point does not have circle ctor, but found {}",
2833 circle.ctor.human_friendly_kind_with_article()
2834 )));
2835 };
2836 let mut circle_ctor = circle_ctor.clone();
2837 if circle.center == point_id {
2838 circle_ctor.center = ctor.position;
2839 } else if circle.start == point_id {
2840 circle_ctor.start = ctor.position;
2841 } else {
2842 return Err(KclError::refactor(format!(
2843 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2844 )));
2845 }
2846 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2847 }
2848
2849 if let Segment::ControlPointSpline(spline) = segment {
2850 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2851 return Err(KclError::refactor(format!(
2852 "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2853 spline.ctor.human_friendly_kind_with_article()
2854 )));
2855 };
2856 let mut spline_ctor = spline_ctor.clone();
2857 let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2858 return Err(KclError::refactor(format!(
2859 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2860 )));
2861 };
2862 spline_ctor.points[control_index] = ctor.position;
2863 return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2864 }
2865
2866 }
2869
2870 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2872 Ok(())
2873 }
2874
2875 fn edit_line(
2876 &mut self,
2877 new_ast: &mut ast::Node<ast::Program>,
2878 sketch: ObjectId,
2879 line: ObjectId,
2880 ctor: LineCtor,
2881 ) -> Result<(), KclError> {
2882 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2884 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2885
2886 let sketch_id = sketch;
2888 let sketch_object = self
2889 .scene_graph
2890 .objects
2891 .get(sketch_id.0)
2892 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2893 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2894 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2895 };
2896 sketch
2897 .segments
2898 .iter()
2899 .find(|o| **o == line)
2900 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2901 let line_id = line;
2903 let line_object = self
2904 .scene_graph
2905 .objects
2906 .get(line_id.0)
2907 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2908 let ObjectKind::Segment { .. } = &line_object.kind else {
2909 let kind = line_object.kind.human_friendly_kind_with_article();
2910 return Err(KclError::refactor(format!(
2911 "This constraint only works on Segments, but you selected {kind}"
2912 )));
2913 };
2914
2915 self.mutate_ast(
2917 new_ast,
2918 line_id,
2919 AstMutateCommand::EditLine {
2920 start: new_start_ast,
2921 end: new_end_ast,
2922 construction: ctor.construction,
2923 },
2924 )?;
2925 Ok(())
2926 }
2927
2928 fn edit_arc(
2929 &mut self,
2930 new_ast: &mut ast::Node<ast::Program>,
2931 sketch: ObjectId,
2932 arc: ObjectId,
2933 ctor: ArcCtor,
2934 ) -> Result<(), KclError> {
2935 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2937 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2938 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2939
2940 let sketch_id = sketch;
2942 let sketch_object = self
2943 .scene_graph
2944 .objects
2945 .get(sketch_id.0)
2946 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2947 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2948 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2949 };
2950 sketch
2951 .segments
2952 .iter()
2953 .find(|o| **o == arc)
2954 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2955 let arc_id = arc;
2957 let arc_object = self
2958 .scene_graph
2959 .objects
2960 .get(arc_id.0)
2961 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2962 let ObjectKind::Segment { .. } = &arc_object.kind else {
2963 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2964 };
2965
2966 self.mutate_ast(
2968 new_ast,
2969 arc_id,
2970 AstMutateCommand::EditArc {
2971 start: new_start_ast,
2972 end: new_end_ast,
2973 center: new_center_ast,
2974 direction: ctor.direction,
2975 construction: ctor.construction,
2976 },
2977 )?;
2978 Ok(())
2979 }
2980
2981 fn edit_circle(
2982 &mut self,
2983 new_ast: &mut ast::Node<ast::Program>,
2984 sketch: ObjectId,
2985 circle: ObjectId,
2986 ctor: CircleCtor,
2987 ) -> Result<(), KclError> {
2988 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2990 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2991
2992 let sketch_id = sketch;
2994 let sketch_object = self
2995 .scene_graph
2996 .objects
2997 .get(sketch_id.0)
2998 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2999 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3000 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3001 };
3002 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3003 KclError::refactor(format!(
3004 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3005 ))
3006 })?;
3007 let circle_id = circle;
3009 let circle_object = self
3010 .scene_graph
3011 .objects
3012 .get(circle_id.0)
3013 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3014 let ObjectKind::Segment { .. } = &circle_object.kind else {
3015 return Err(KclError::refactor(format!(
3016 "Object is not a segment: {circle_object:?}"
3017 )));
3018 };
3019
3020 self.mutate_ast(
3022 new_ast,
3023 circle_id,
3024 AstMutateCommand::EditCircle {
3025 start: new_start_ast,
3026 center: new_center_ast,
3027 construction: ctor.construction,
3028 },
3029 )?;
3030 Ok(())
3031 }
3032
3033 fn edit_control_point_spline(
3034 &mut self,
3035 new_ast: &mut ast::Node<ast::Program>,
3036 sketch: ObjectId,
3037 spline: ObjectId,
3038 ctor: ControlPointSplineCtor,
3039 ) -> Result<(), KclError> {
3040 let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3041
3042 let sketch_object = self
3043 .scene_graph
3044 .objects
3045 .get(sketch.0)
3046 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3047 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3048 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3049 };
3050 sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3051 KclError::refactor(format!(
3052 "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3053 ))
3054 })?;
3055
3056 let spline_object =
3057 self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3058 KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3059 })?;
3060 let ObjectKind::Segment { .. } = &spline_object.kind else {
3061 return Err(KclError::refactor(format!(
3062 "Object is not a segment: {spline_object:?}"
3063 )));
3064 };
3065
3066 self.mutate_ast(
3067 new_ast,
3068 spline,
3069 AstMutateCommand::EditControlPointSpline {
3070 points: points_ast,
3071 construction: ctor.construction,
3072 },
3073 )?;
3074 Ok(())
3075 }
3076
3077 fn delete_segment(
3078 &mut self,
3079 new_ast: &mut ast::Node<ast::Program>,
3080 sketch: ObjectId,
3081 segment_id: ObjectId,
3082 ) -> Result<(), KclError> {
3083 let sketch_id = sketch;
3085 let sketch_object = self
3086 .scene_graph
3087 .objects
3088 .get(sketch_id.0)
3089 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3090 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3091 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3092 };
3093 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3094 KclError::refactor(format!(
3095 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3096 ))
3097 })?;
3098 let segment_object =
3100 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3101 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3102 })?;
3103 let ObjectKind::Segment { .. } = &segment_object.kind else {
3104 return Err(KclError::refactor(format!(
3105 "Object is not a segment, it is {}",
3106 segment_object.kind.human_friendly_kind_with_article()
3107 )));
3108 };
3109
3110 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3112 Ok(())
3113 }
3114
3115 fn delete_constraint(
3116 &mut self,
3117 new_ast: &mut ast::Node<ast::Program>,
3118 sketch: ObjectId,
3119 constraint_id: ObjectId,
3120 ) -> Result<(), KclError> {
3121 let sketch_id = sketch;
3123 let sketch_object = self
3124 .scene_graph
3125 .objects
3126 .get(sketch_id.0)
3127 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3128 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3129 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3130 };
3131 sketch
3132 .constraints
3133 .iter()
3134 .find(|o| **o == constraint_id)
3135 .ok_or_else(|| {
3136 KclError::refactor(format!(
3137 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3138 ))
3139 })?;
3140 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3142 KclError::refactor(format!(
3143 "Constraint not found in scene graph: constraint={constraint_id:?}"
3144 ))
3145 })?;
3146 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3147 return Err(KclError::refactor(format!(
3148 "Object is not a constraint, it is {}",
3149 constraint_object.kind.human_friendly_kind_with_article()
3150 )));
3151 };
3152
3153 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3155 Ok(())
3156 }
3157
3158 fn edit_coincident_constraint(
3159 &mut self,
3160 new_ast: &mut ast::Node<ast::Program>,
3161 constraint_id: ObjectId,
3162 segments: Vec<ConstraintSegment>,
3163 ) -> Result<(), KclError> {
3164 if segments.len() < 2 {
3165 return Err(KclError::refactor(format!(
3166 "Coincident constraint must have at least 2 inputs, got {}",
3167 segments.len()
3168 )));
3169 }
3170
3171 let segment_asts = segments
3172 .iter()
3173 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3174 .collect::<Result<Vec<_>, _>>()?;
3175
3176 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3177 elements: segment_asts,
3178 digest: None,
3179 non_code_meta: Default::default(),
3180 })));
3181
3182 self.mutate_ast(
3183 new_ast,
3184 constraint_id,
3185 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3186 )?;
3187 Ok(())
3188 }
3189
3190 fn edit_horizontal_points_constraint(
3191 &mut self,
3192 new_ast: &mut ast::Node<ast::Program>,
3193 constraint_id: ObjectId,
3194 points: Vec<ConstraintSegment>,
3195 ) -> Result<(), KclError> {
3196 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3197 }
3198
3199 fn edit_vertical_points_constraint(
3200 &mut self,
3201 new_ast: &mut ast::Node<ast::Program>,
3202 constraint_id: ObjectId,
3203 points: Vec<ConstraintSegment>,
3204 ) -> Result<(), KclError> {
3205 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3206 }
3207
3208 fn edit_axis_points_constraint(
3209 &mut self,
3210 new_ast: &mut ast::Node<ast::Program>,
3211 constraint_id: ObjectId,
3212 points: Vec<ConstraintSegment>,
3213 constraint_name: &str,
3214 ) -> Result<(), KclError> {
3215 if points.len() < 2 {
3216 return Err(KclError::refactor(format!(
3217 "{constraint_name} points constraint must have at least 2 points, got {}",
3218 points.len()
3219 )));
3220 }
3221
3222 let point_asts = points
3223 .iter()
3224 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3225 .collect::<Result<Vec<_>, _>>()?;
3226
3227 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3228 elements: point_asts,
3229 digest: None,
3230 non_code_meta: Default::default(),
3231 })));
3232
3233 self.mutate_ast(
3234 new_ast,
3235 constraint_id,
3236 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3237 )?;
3238 Ok(())
3239 }
3240
3241 fn edit_equal_length_constraint(
3243 &mut self,
3244 new_ast: &mut ast::Node<ast::Program>,
3245 constraint_id: ObjectId,
3246 lines: Vec<ObjectId>,
3247 ) -> Result<(), KclError> {
3248 if lines.len() < 2 {
3249 return Err(KclError::refactor(format!(
3250 "Lines equal length constraint must have at least 2 lines, got {}",
3251 lines.len()
3252 )));
3253 }
3254
3255 let line_asts = lines
3256 .iter()
3257 .map(|line_id| {
3258 let line_object = self
3259 .scene_graph
3260 .objects
3261 .get(line_id.0)
3262 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3263 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3264 let kind = line_object.kind.human_friendly_kind_with_article();
3265 return Err(KclError::refactor(format!(
3266 "This constraint only works on Segments, but you selected {kind}"
3267 )));
3268 };
3269 let Segment::Line(_) = line_segment else {
3270 let kind = line_segment.human_friendly_kind_with_article();
3271 return Err(KclError::refactor(format!(
3272 "Only lines can be made equal length, but you selected {kind}"
3273 )));
3274 };
3275
3276 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3277 })
3278 .collect::<Result<Vec<_>, _>>()?;
3279
3280 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3281 elements: line_asts,
3282 digest: None,
3283 non_code_meta: Default::default(),
3284 })));
3285
3286 self.mutate_ast(
3287 new_ast,
3288 constraint_id,
3289 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3290 )?;
3291 Ok(())
3292 }
3293
3294 fn edit_parallel_constraint(
3296 &mut self,
3297 new_ast: &mut ast::Node<ast::Program>,
3298 constraint_id: ObjectId,
3299 lines: Vec<ObjectId>,
3300 ) -> Result<(), KclError> {
3301 if lines.len() < 2 {
3302 return Err(KclError::refactor(format!(
3303 "Parallel constraint must have at least 2 lines, got {}",
3304 lines.len()
3305 )));
3306 }
3307
3308 let line_asts = lines
3309 .iter()
3310 .map(|line_id| {
3311 let line_object = self
3312 .scene_graph
3313 .objects
3314 .get(line_id.0)
3315 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3316 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3317 let kind = line_object.kind.human_friendly_kind_with_article();
3318 return Err(KclError::refactor(format!(
3319 "This constraint only works on Segments, but you selected {kind}"
3320 )));
3321 };
3322 let Segment::Line(_) = line_segment else {
3323 let kind = line_segment.human_friendly_kind_with_article();
3324 return Err(KclError::refactor(format!(
3325 "Only lines can be made parallel, but you selected {kind}"
3326 )));
3327 };
3328
3329 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3330 })
3331 .collect::<Result<Vec<_>, _>>()?;
3332
3333 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3334 elements: line_asts,
3335 digest: None,
3336 non_code_meta: Default::default(),
3337 })));
3338
3339 self.mutate_ast(
3340 new_ast,
3341 constraint_id,
3342 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3343 )?;
3344 Ok(())
3345 }
3346
3347 fn edit_equal_radius_constraint(
3349 &mut self,
3350 new_ast: &mut ast::Node<ast::Program>,
3351 constraint_id: ObjectId,
3352 input: Vec<ObjectId>,
3353 ) -> Result<(), KclError> {
3354 if input.len() < 2 {
3355 return Err(KclError::refactor(format!(
3356 "equalRadius constraint must have at least 2 segments, got {}",
3357 input.len()
3358 )));
3359 }
3360
3361 let input_asts = input
3362 .iter()
3363 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3364 .collect::<Result<Vec<_>, _>>()?;
3365
3366 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3367 elements: input_asts,
3368 digest: None,
3369 non_code_meta: Default::default(),
3370 })));
3371
3372 self.mutate_ast(
3373 new_ast,
3374 constraint_id,
3375 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3376 )?;
3377 Ok(())
3378 }
3379
3380 async fn execute_after_edit(
3381 &mut self,
3382 ctx: &ExecutorContext,
3383 sketch: ObjectId,
3384 sketch_block_ref: AstNodeRef,
3385 new_ast: &mut ast::Node<ast::Program>,
3386 options: ExecuteAfterEditOptions,
3387 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3388 let ExecuteAfterEditOptions {
3389 segment_ids_edited,
3390 edit_kind,
3391 commit_solved_initial_guesses,
3392 } = options;
3393
3394 let new_source = source_from_ast(new_ast);
3396 let new_program = parse_frontend_mutation_source(
3398 &new_source,
3399 "Error parsing KCL source after editing",
3400 "No AST produced after editing",
3401 )?;
3402
3403 let is_delete = edit_kind.is_delete();
3405 let truncated_program = {
3406 let mut truncated_program = new_program.clone();
3407 only_sketch_block(
3408 &mut truncated_program.ast,
3409 &sketch_block_ref,
3410 edit_kind.to_change_kind(),
3411 )
3412 .map_err(KclErrorWithOutputs::no_outputs)?;
3413 truncated_program
3414 };
3415
3416 let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3418 let mock_config = MockConfig {
3419 sketch_block_id: Some(sketch),
3420 freedom_analysis: is_delete,
3421 segment_ids_edited: segment_ids_edited.clone(),
3422 drag_anchors,
3423 ..Default::default()
3424 };
3425 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3426
3427 self.program = new_program;
3429
3430 let outcome = self.update_state_after_exec(outcome, is_delete);
3432
3433 let src_delta = if commit_solved_initial_guesses {
3434 self.commit_var_solutions_to_program(&outcome, "editing")?
3435 } else {
3436 SourceDelta { text: new_source }
3437 };
3438 let scene_graph_delta = SceneGraphDelta {
3439 new_graph: self.scene_graph_for_ui(),
3440 invalidates_ids: is_delete,
3441 new_objects: Vec::new(),
3442 exec_outcome: outcome,
3443 };
3444 Ok((src_delta, scene_graph_delta))
3445 }
3446
3447 async fn execute_after_delete_sketch(
3448 &mut self,
3449 ctx: &ExecutorContext,
3450 new_ast: &mut ast::Node<ast::Program>,
3451 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3452 let new_source = source_from_ast(new_ast);
3454 let new_program = parse_frontend_mutation_source(
3456 &new_source,
3457 "Error parsing KCL source after editing",
3458 "No AST produced after editing",
3459 )?;
3460
3461 self.program = new_program.clone();
3463
3464 let outcome = ctx.run_with_caching(new_program).await?;
3470 let freedom_analysis_ran = true;
3471
3472 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3473
3474 let src_delta = SourceDelta { text: new_source };
3475 let scene_graph_delta = SceneGraphDelta {
3476 new_graph: self.scene_graph_for_ui(),
3477 invalidates_ids: true,
3478 new_objects: Vec::new(),
3479 exec_outcome: outcome,
3480 };
3481 Ok((src_delta, scene_graph_delta))
3482 }
3483
3484 fn point_id_to_ast_reference(
3489 &self,
3490 point_id: ObjectId,
3491 new_ast: &mut ast::Node<ast::Program>,
3492 ) -> Result<ast::Expr, KclError> {
3493 let point_object = self
3494 .scene_graph
3495 .objects
3496 .get(point_id.0)
3497 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3498 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3499 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3500 };
3501 let Segment::Point(point) = point_segment else {
3502 return Err(KclError::refactor(format!(
3503 "Only points are currently supported: {point_object:?}"
3504 )));
3505 };
3506
3507 if let Some(owner_id) = point.owner {
3508 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3509 KclError::refactor(format!(
3510 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3511 ))
3512 })?;
3513 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3514 return Err(KclError::refactor(format!(
3515 "Owner of point is not a segment, but found {}",
3516 owner_object.kind.human_friendly_kind_with_article()
3517 )));
3518 };
3519
3520 match owner_segment {
3521 Segment::Line(line) => {
3522 let property = if line.start == point_id {
3523 LINE_PROPERTY_START
3524 } else if line.end == point_id {
3525 LINE_PROPERTY_END
3526 } else {
3527 return Err(KclError::refactor(format!(
3528 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3529 )));
3530 };
3531 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3532 }
3533 Segment::Arc(arc) => {
3534 let property = if arc.start == point_id {
3535 ARC_PROPERTY_START
3536 } else if arc.end == point_id {
3537 ARC_PROPERTY_END
3538 } else if arc.center == point_id {
3539 ARC_PROPERTY_CENTER
3540 } else {
3541 return Err(KclError::refactor(format!(
3542 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3543 )));
3544 };
3545 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3546 }
3547 Segment::Circle(circle) => {
3548 let property = if circle.start == point_id {
3549 CIRCLE_PROPERTY_START
3550 } else if circle.center == point_id {
3551 CIRCLE_PROPERTY_CENTER
3552 } else {
3553 return Err(KclError::refactor(format!(
3554 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3555 )));
3556 };
3557 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3558 }
3559 Segment::ControlPointSpline(spline) => {
3560 let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3561 return Err(KclError::refactor(format!(
3562 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3563 )));
3564 };
3565 let owner_expr =
3566 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3567 let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3568 Ok(create_index_expression(controls_expr, index))
3569 }
3570 _ => Err(KclError::refactor(format!(
3571 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3572 ))),
3573 }
3574 } else {
3575 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3577 }
3578 }
3579
3580 fn line_id_to_ast_reference(
3581 &self,
3582 line_id: ObjectId,
3583 new_ast: &mut ast::Node<ast::Program>,
3584 ) -> Result<ast::Expr, KclError> {
3585 let line_object = self
3586 .scene_graph
3587 .objects
3588 .get(line_id.0)
3589 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3590 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3591 return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3592 };
3593 let Segment::Line(line) = line_segment else {
3594 return Err(KclError::refactor(format!(
3595 "Only lines are currently supported: {line_object:?}"
3596 )));
3597 };
3598
3599 if let Some(owner_id) = line.owner {
3600 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3601 KclError::refactor(format!(
3602 "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3603 ))
3604 })?;
3605 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3606 return Err(KclError::refactor(format!(
3607 "Owner of line is not a segment, but found {}",
3608 owner_object.kind.human_friendly_kind_with_article()
3609 )));
3610 };
3611
3612 match owner_segment {
3613 Segment::ControlPointSpline(spline) => {
3614 let Some(index) = spline
3615 .controls
3616 .windows(2)
3617 .position(|window| window[0] == line.start && window[1] == line.end)
3618 else {
3619 return Err(KclError::refactor(format!(
3620 "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3621 )));
3622 };
3623 let owner_expr =
3624 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3625 let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3626 Ok(create_index_expression(edges_expr, index))
3627 }
3628 _ => Err(KclError::refactor(format!(
3629 "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3630 ))),
3631 }
3632 } else {
3633 get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3634 }
3635 }
3636
3637 fn coincident_segment_to_ast(
3638 &self,
3639 segment: &ConstraintSegment,
3640 new_ast: &mut ast::Node<ast::Program>,
3641 ) -> Result<ast::Expr, KclError> {
3642 match segment {
3643 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3644 ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3645 }
3646 }
3647
3648 fn segment_id_to_constraint_ast_reference(
3649 &self,
3650 segment_id: ObjectId,
3651 new_ast: &mut ast::Node<ast::Program>,
3652 ) -> Result<ast::Expr, KclError> {
3653 let segment_object = self
3654 .scene_graph
3655 .objects
3656 .get(segment_id.0)
3657 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3658 let ObjectKind::Segment { segment } = &segment_object.kind else {
3659 return Err(KclError::refactor(format!(
3660 "Object is not a segment, it is {}",
3661 segment_object.kind.human_friendly_kind_with_article()
3662 )));
3663 };
3664
3665 match segment {
3666 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3667 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3668 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3669 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3670 Segment::ControlPointSpline(_) => {
3671 get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3672 }
3673 }
3674 }
3675
3676 fn axis_constraint_segment_to_ast(
3677 &self,
3678 segment: &ConstraintSegment,
3679 new_ast: &mut ast::Node<ast::Program>,
3680 ) -> Result<ast::Expr, KclError> {
3681 match segment {
3682 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3683 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3684 }
3685 }
3686
3687 async fn add_coincident(
3688 &mut self,
3689 sketch: ObjectId,
3690 coincident: Coincident,
3691 new_ast: &mut ast::Node<ast::Program>,
3692 ) -> Result<AstNodeRef, KclError> {
3693 let sketch_id = sketch;
3694 for segment in &coincident.segments {
3695 let ConstraintSegment::Segment(segment_id) = segment else {
3696 continue;
3697 };
3698 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3699 continue;
3700 };
3701 if matches!(
3702 segment_object.kind,
3703 ObjectKind::Segment {
3704 segment: Segment::ControlPointSpline(_)
3705 }
3706 ) {
3707 return Err(KclError::refactor(
3708 "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3709 .to_owned(),
3710 ));
3711 }
3712 }
3713 let segment_asts = coincident
3714 .segments
3715 .iter()
3716 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3717 .collect::<Result<Vec<_>, _>>()?;
3718 if segment_asts.len() < 2 {
3719 return Err(KclError::refactor(format!(
3720 "Coincident constraint must have at least 2 inputs, got {}",
3721 segment_asts.len()
3722 )));
3723 }
3724
3725 let coincident_ast = create_coincident_ast(segment_asts);
3727
3728 let (sketch_block_ref, _) = self.mutate_ast(
3730 new_ast,
3731 sketch_id,
3732 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3733 )?;
3734 Ok(sketch_block_ref)
3735 }
3736
3737 async fn add_distance(
3738 &mut self,
3739 sketch: ObjectId,
3740 distance: Distance,
3741 new_ast: &mut ast::Node<ast::Program>,
3742 ) -> Result<AstNodeRef, KclError> {
3743 self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3744 }
3745
3746 fn distance_constraint_ast_parts(
3747 &self,
3748 function_name: &str,
3749 distance: &Distance,
3750 new_ast: &mut ast::Node<ast::Program>,
3751 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3752 let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3753 [pt0, pt1] => [
3754 self.coincident_segment_to_ast(pt0, new_ast)?,
3755 self.coincident_segment_to_ast(pt1, new_ast)?,
3756 ],
3757 _ => {
3758 return Err(KclError::refactor(format!(
3759 "Distance constraint must have exactly 2 segments, got {}",
3760 distance.segments.len()
3761 )));
3762 }
3763 };
3764
3765 let arguments = match &distance.label_position {
3766 Some(label_position) => vec![ast::LabeledArg {
3767 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3768 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3769 }],
3770 None => Default::default(),
3771 };
3772
3773 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3774 callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3775 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3776 ast::ArrayExpression {
3777 elements: vec![segment0_ast, segment1_ast],
3778 digest: None,
3779 non_code_meta: Default::default(),
3780 },
3781 )))),
3782 arguments,
3783 digest: None,
3784 non_code_meta: Default::default(),
3785 })));
3786 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3787 value: ast::LiteralValue::Number {
3788 value: distance.distance.value,
3789 suffix: distance.distance.units,
3790 },
3791 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3792 KclError::refactor(format!(
3793 "Could not format numeric suffix: {:?}",
3794 distance.distance.units
3795 ))
3796 })?,
3797 digest: None,
3798 })));
3799
3800 Ok((call, value))
3801 }
3802
3803 fn add_distance_constraint(
3804 &mut self,
3805 sketch: ObjectId,
3806 function_name: &str,
3807 distance: Distance,
3808 new_ast: &mut ast::Node<ast::Program>,
3809 ) -> Result<AstNodeRef, KclError> {
3810 let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3811 let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3812 left: call,
3813 operator: ast::BinaryOperator::Eq,
3814 right: value,
3815 digest: None,
3816 })));
3817
3818 let (sketch_block_ref, _) = self.mutate_ast(
3819 new_ast,
3820 sketch,
3821 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3822 )?;
3823 Ok(sketch_block_ref)
3824 }
3825
3826 async fn add_angle(
3827 &mut self,
3828 sketch: ObjectId,
3829 angle: Angle,
3830 new_ast: &mut ast::Node<ast::Program>,
3831 ) -> Result<AstNodeRef, KclError> {
3832 let sketch_id = sketch;
3833 let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3834 let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3835 left: angle_call_ast,
3836 operator: ast::BinaryOperator::Eq,
3837 right: angle_value_ast,
3838 digest: None,
3839 })));
3840
3841 let (sketch_block_ref, _) = self.mutate_ast(
3843 new_ast,
3844 sketch_id,
3845 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3846 )?;
3847 Ok(sketch_block_ref)
3848 }
3849
3850 fn angle_constraint_ast_parts(
3851 &self,
3852 angle: &Angle,
3853 new_ast: &mut ast::Node<ast::Program>,
3854 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3855 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3856 return Err(KclError::refactor(format!(
3857 "Angle constraint must have exactly 2 lines, got {}",
3858 angle.lines.len()
3859 )));
3860 };
3861
3862 let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3863 let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3864 let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3865 elements: vec![l0_ast, l1_ast],
3866 digest: None,
3867 non_code_meta: Default::default(),
3868 })));
3869
3870 if angle.inverse == Some(true) && angle.sector.is_none() {
3871 return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3872 }
3873
3874 let uses_angle_dimension = angle.sector.is_some();
3875 let mut arguments = if uses_angle_dimension {
3876 vec![ast::LabeledArg {
3877 label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3878 arg: lines_ast.clone(),
3879 }]
3880 } else {
3881 Default::default()
3882 };
3883
3884 if let Some(sector) = angle.sector {
3885 arguments.push(ast::LabeledArg {
3886 label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3887 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3888 value: ast::LiteralValue::Number {
3889 value: f64::from(sector),
3890 suffix: NumericSuffix::None,
3891 },
3892 raw: sector.to_string(),
3893 digest: None,
3894 }))),
3895 });
3896 }
3897
3898 if angle.inverse == Some(true) {
3899 arguments.push(ast::LabeledArg {
3900 label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3901 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3902 value: ast::LiteralValue::Bool(true),
3903 raw: true.to_string(),
3904 digest: None,
3905 }))),
3906 });
3907 }
3908
3909 if let Some(label_position) = &angle.label_position {
3910 arguments.push(ast::LabeledArg {
3911 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3912 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3913 });
3914 }
3915
3916 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3917 callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3918 ANGLE_DIMENSION_FN
3919 } else {
3920 ANGLE_FN
3921 })),
3922 unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3923 arguments,
3924 digest: None,
3925 non_code_meta: Default::default(),
3926 })));
3927 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3928 value: ast::LiteralValue::Number {
3929 value: angle.angle.value,
3930 suffix: angle.angle.units,
3931 },
3932 raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3933 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3934 digest: None,
3935 })));
3936
3937 Ok((call, value))
3938 }
3939
3940 async fn add_tangent(
3941 &mut self,
3942 sketch: ObjectId,
3943 tangent: Tangent,
3944 new_ast: &mut ast::Node<ast::Program>,
3945 ) -> Result<AstNodeRef, KclError> {
3946 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3947 return Err(KclError::refactor(format!(
3948 "Tangent constraint must have exactly 2 segments, got {}",
3949 tangent.input.len()
3950 )));
3951 };
3952 let sketch_id = sketch;
3953
3954 let seg0_object = self
3955 .scene_graph
3956 .objects
3957 .get(seg0_id.0)
3958 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3959 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3960 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3961 };
3962 let seg0_ast = match seg0_segment {
3963 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3964 self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3965 }
3966 _ => {
3967 return Err(KclError::refactor(format!(
3968 "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3969 )));
3970 }
3971 };
3972
3973 let seg1_object = self
3974 .scene_graph
3975 .objects
3976 .get(seg1_id.0)
3977 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3978 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3979 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3980 };
3981 let seg1_ast = match seg1_segment {
3982 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3983 self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3984 }
3985 _ => {
3986 return Err(KclError::refactor(format!(
3987 "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3988 )));
3989 }
3990 };
3991
3992 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3993 let (sketch_block_ref, _) = self.mutate_ast(
3994 new_ast,
3995 sketch_id,
3996 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3997 )?;
3998 Ok(sketch_block_ref)
3999 }
4000
4001 async fn add_symmetric(
4002 &mut self,
4003 sketch: ObjectId,
4004 symmetric: Symmetric,
4005 new_ast: &mut ast::Node<ast::Program>,
4006 ) -> Result<AstNodeRef, KclError> {
4007 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4008 return Err(KclError::refactor(format!(
4009 "Symmetric constraint must have exactly 2 inputs, got {}",
4010 symmetric.input.len()
4011 )));
4012 };
4013 let sketch_id = sketch;
4014
4015 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4016 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4017 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4018
4019 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4020 let (sketch_block_ref, _) = self.mutate_ast(
4021 new_ast,
4022 sketch_id,
4023 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4024 )?;
4025 Ok(sketch_block_ref)
4026 }
4027
4028 async fn add_midpoint(
4029 &mut self,
4030 sketch: ObjectId,
4031 midpoint: Midpoint,
4032 new_ast: &mut ast::Node<ast::Program>,
4033 ) -> Result<AstNodeRef, KclError> {
4034 let sketch_id = sketch;
4035 let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4036
4037 let segment_object = self
4038 .scene_graph
4039 .objects
4040 .get(midpoint.segment.0)
4041 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4042 let ObjectKind::Segment {
4043 segment: midpoint_segment,
4044 } = &segment_object.kind
4045 else {
4046 return Err(KclError::refactor(format!(
4047 "Object must be a segment, but it was {}",
4048 segment_object.kind.human_friendly_kind_with_article()
4049 )));
4050 };
4051 let segment_ast = match midpoint_segment {
4052 Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4053 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4054 _ => {
4055 return Err(KclError::refactor(format!(
4056 "Midpoint target must be a line or arc segment but it was {}",
4057 midpoint_segment.human_friendly_kind_with_article()
4058 )));
4059 }
4060 };
4061
4062 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4063 let (sketch_block_ref, _) = self.mutate_ast(
4064 new_ast,
4065 sketch_id,
4066 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4067 )?;
4068 Ok(sketch_block_ref)
4069 }
4070
4071 async fn add_equal_radius(
4072 &mut self,
4073 sketch: ObjectId,
4074 equal_radius: EqualRadius,
4075 new_ast: &mut ast::Node<ast::Program>,
4076 ) -> Result<AstNodeRef, KclError> {
4077 if equal_radius.input.len() < 2 {
4078 return Err(KclError::refactor(format!(
4079 "equalRadius constraint must have at least 2 segments, got {}",
4080 equal_radius.input.len()
4081 )));
4082 }
4083
4084 let sketch_id = sketch;
4085 let input_asts = equal_radius
4086 .input
4087 .iter()
4088 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4089 .collect::<Result<Vec<_>, _>>()?;
4090
4091 let equal_radius_ast = create_equal_radius_ast(input_asts);
4092 let (sketch_block_ref, _) = self.mutate_ast(
4093 new_ast,
4094 sketch_id,
4095 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4096 )?;
4097 Ok(sketch_block_ref)
4098 }
4099
4100 async fn add_radius(
4101 &mut self,
4102 sketch: ObjectId,
4103 radius: Radius,
4104 new_ast: &mut ast::Node<ast::Program>,
4105 ) -> Result<AstNodeRef, KclError> {
4106 let params = ArcSizeConstraintParams {
4107 points: vec![radius.arc],
4108 function_name: RADIUS_FN,
4109 value: radius.radius.value,
4110 units: radius.radius.units,
4111 label_position: radius.label_position,
4112 constraint_type_name: "Radius",
4113 };
4114 self.add_arc_size_constraint(sketch, params, new_ast).await
4115 }
4116
4117 async fn add_diameter(
4118 &mut self,
4119 sketch: ObjectId,
4120 diameter: Diameter,
4121 new_ast: &mut ast::Node<ast::Program>,
4122 ) -> Result<AstNodeRef, KclError> {
4123 let params = ArcSizeConstraintParams {
4124 points: vec![diameter.arc],
4125 function_name: DIAMETER_FN,
4126 value: diameter.diameter.value,
4127 units: diameter.diameter.units,
4128 label_position: diameter.label_position,
4129 constraint_type_name: "Diameter",
4130 };
4131 self.add_arc_size_constraint(sketch, params, new_ast).await
4132 }
4133
4134 async fn add_fixed_constraints(
4135 &mut self,
4136 sketch: ObjectId,
4137 points: Vec<FixedPoint>,
4138 new_ast: &mut ast::Node<ast::Program>,
4139 ) -> Result<AstNodeRef, KclError> {
4140 let mut sketch_block_ref = None;
4141
4142 for fixed_point in points {
4143 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4144 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4145 .map_err(|err| KclError::refactor(err.to_string()))?;
4146
4147 let (sketch_ref, _) = self.mutate_ast(
4148 new_ast,
4149 sketch,
4150 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4151 )?;
4152 sketch_block_ref = Some(sketch_ref);
4153 }
4154
4155 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4156 }
4157
4158 async fn add_arc_size_constraint(
4159 &mut self,
4160 sketch: ObjectId,
4161 params: ArcSizeConstraintParams,
4162 new_ast: &mut ast::Node<ast::Program>,
4163 ) -> Result<AstNodeRef, KclError> {
4164 let sketch_id = sketch;
4165
4166 if params.points.len() != 1 {
4168 return Err(KclError::refactor(format!(
4169 "{} constraint must have exactly 1 argument (an arc segment), got {}",
4170 params.constraint_type_name,
4171 params.points.len()
4172 )));
4173 }
4174
4175 let arc_id = params.points[0];
4176 let arc_object = self
4177 .scene_graph
4178 .objects
4179 .get(arc_id.0)
4180 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4181 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4182 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4183 };
4184 let ref_type = match arc_segment {
4185 Segment::Arc(_) => ARC_VARIABLE,
4186 Segment::Circle(_) => CIRCLE_VARIABLE,
4187 _ => {
4188 return Err(KclError::refactor(format!(
4189 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4190 params.constraint_type_name
4191 )));
4192 }
4193 };
4194 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4196 let arguments = match ¶ms.label_position {
4197 Some(label_position) => vec![ast::LabeledArg {
4198 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4199 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4200 }],
4201 None => Default::default(),
4202 };
4203
4204 let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4206 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4207 unlabeled: Some(arc_ast),
4208 arguments,
4209 digest: None,
4210 non_code_meta: Default::default(),
4211 })));
4212 let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4213 left: call_ast,
4214 operator: ast::BinaryOperator::Eq,
4215 right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4216 value: ast::LiteralValue::Number {
4217 value: params.value,
4218 suffix: params.units,
4219 },
4220 raw: format_number_literal(params.value, params.units, None)
4221 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4222 digest: None,
4223 }))),
4224 digest: None,
4225 })));
4226
4227 let (sketch_block_ref, _) = self.mutate_ast(
4229 new_ast,
4230 sketch_id,
4231 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4232 )?;
4233 Ok(sketch_block_ref)
4234 }
4235
4236 async fn add_horizontal_distance(
4237 &mut self,
4238 sketch: ObjectId,
4239 distance: Distance,
4240 new_ast: &mut ast::Node<ast::Program>,
4241 ) -> Result<AstNodeRef, KclError> {
4242 self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4243 }
4244
4245 async fn add_vertical_distance(
4246 &mut self,
4247 sketch: ObjectId,
4248 distance: Distance,
4249 new_ast: &mut ast::Node<ast::Program>,
4250 ) -> Result<AstNodeRef, KclError> {
4251 self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4252 }
4253
4254 async fn add_horizontal(
4255 &mut self,
4256 sketch: ObjectId,
4257 horizontal: Horizontal,
4258 new_ast: &mut ast::Node<ast::Program>,
4259 ) -> Result<AstNodeRef, KclError> {
4260 let sketch_id = sketch;
4261
4262 let first_arg_ast = match horizontal {
4264 Horizontal::Line { line } => {
4265 let line_object = self
4266 .scene_graph
4267 .objects
4268 .get(line.0)
4269 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4270 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4271 let kind = line_object.kind.human_friendly_kind_with_article();
4272 return Err(KclError::refactor(format!(
4273 "This constraint only works on Segments, but you selected {kind}"
4274 )));
4275 };
4276 let Segment::Line(_) = line_segment else {
4277 return Err(KclError::refactor(format!(
4278 "Only lines can be made horizontal, but you selected {}",
4279 line_segment.human_friendly_kind_with_article(),
4280 )));
4281 };
4282 self.line_id_to_ast_reference(line, new_ast)?
4283 }
4284 Horizontal::Points { points } => {
4285 let point_asts = points
4286 .iter()
4287 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4288 .collect::<Result<Vec<_>, _>>()?;
4289 ast::ArrayExpression::new(point_asts).into()
4290 }
4291 };
4292 let horizontal_ast = create_horizontal_ast(first_arg_ast);
4294
4295 let (sketch_block_ref, _) = self.mutate_ast(
4297 new_ast,
4298 sketch_id,
4299 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4300 )?;
4301 Ok(sketch_block_ref)
4302 }
4303
4304 async fn add_lines_equal_length(
4305 &mut self,
4306 sketch: ObjectId,
4307 lines_equal_length: LinesEqualLength,
4308 new_ast: &mut ast::Node<ast::Program>,
4309 ) -> Result<AstNodeRef, KclError> {
4310 if lines_equal_length.lines.len() < 2 {
4311 return Err(KclError::refactor(format!(
4312 "Lines equal length constraint must have at least 2 lines, got {}",
4313 lines_equal_length.lines.len()
4314 )));
4315 };
4316
4317 let sketch_id = sketch;
4318
4319 let line_asts = lines_equal_length
4321 .lines
4322 .iter()
4323 .map(|line_id| {
4324 let line_object = self
4325 .scene_graph
4326 .objects
4327 .get(line_id.0)
4328 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4329 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4330 let kind = line_object.kind.human_friendly_kind_with_article();
4331 return Err(KclError::refactor(format!(
4332 "This constraint only works on Segments, but you selected {kind}"
4333 )));
4334 };
4335 let Segment::Line(_) = line_segment else {
4336 let kind = line_segment.human_friendly_kind_with_article();
4337 return Err(KclError::refactor(format!(
4338 "Only lines can be made equal length, but you selected {kind}"
4339 )));
4340 };
4341
4342 self.line_id_to_ast_reference(*line_id, new_ast)
4343 })
4344 .collect::<Result<Vec<_>, _>>()?;
4345
4346 let equal_length_ast = create_equal_length_ast(line_asts);
4348
4349 let (sketch_block_ref, _) = self.mutate_ast(
4351 new_ast,
4352 sketch_id,
4353 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4354 )?;
4355 Ok(sketch_block_ref)
4356 }
4357
4358 fn equal_radius_segment_id_to_ast_reference(
4359 &mut self,
4360 segment_id: ObjectId,
4361 new_ast: &mut ast::Node<ast::Program>,
4362 ) -> Result<ast::Expr, KclError> {
4363 let segment_object = self
4364 .scene_graph
4365 .objects
4366 .get(segment_id.0)
4367 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4368 let ObjectKind::Segment { segment } = &segment_object.kind else {
4369 return Err(KclError::refactor(format!(
4370 "Object is not a segment, it was {}",
4371 segment_object.kind.human_friendly_kind_with_article()
4372 )));
4373 };
4374
4375 let ref_type = match segment {
4376 Segment::Arc(_) => ARC_VARIABLE,
4377 Segment::Circle(_) => CIRCLE_VARIABLE,
4378 _ => {
4379 return Err(KclError::refactor(format!(
4380 "equalRadius supports only arc/circle segments, got {}",
4381 segment.human_friendly_kind_with_article()
4382 )));
4383 }
4384 };
4385
4386 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4387 }
4388
4389 fn symmetric_input_id_to_ast_reference(
4390 &mut self,
4391 segment_id: ObjectId,
4392 new_ast: &mut ast::Node<ast::Program>,
4393 ) -> Result<ast::Expr, KclError> {
4394 let segment_object = self
4395 .scene_graph
4396 .objects
4397 .get(segment_id.0)
4398 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4399 let ObjectKind::Segment { segment } = &segment_object.kind else {
4400 return Err(KclError::refactor(format!(
4401 "Object is not a segment, it was {}",
4402 segment_object.kind.human_friendly_kind_with_article()
4403 )));
4404 };
4405
4406 match segment {
4407 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4408 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4409 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4410 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4411 Segment::ControlPointSpline(_) => Err(KclError::refactor(
4412 "Symmetric does not yet support control point splines".to_owned(),
4413 )),
4414 }
4415 }
4416
4417 fn symmetric_axis_id_to_ast_reference(
4418 &mut self,
4419 segment_id: ObjectId,
4420 new_ast: &mut ast::Node<ast::Program>,
4421 ) -> Result<ast::Expr, KclError> {
4422 let segment_object = self
4423 .scene_graph
4424 .objects
4425 .get(segment_id.0)
4426 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4427 let ObjectKind::Segment { segment } = &segment_object.kind else {
4428 return Err(KclError::refactor(format!(
4429 "Object is not a segment, it was {}",
4430 segment_object.kind.human_friendly_kind_with_article()
4431 )));
4432 };
4433 match segment {
4434 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4435 _ => Err(KclError::refactor(format!(
4436 "Symmetric axis must be a line, got {}",
4437 segment.human_friendly_kind_with_article()
4438 ))),
4439 }
4440 }
4441
4442 async fn add_parallel(
4443 &mut self,
4444 sketch: ObjectId,
4445 parallel: Parallel,
4446 new_ast: &mut ast::Node<ast::Program>,
4447 ) -> Result<AstNodeRef, KclError> {
4448 if parallel.lines.len() < 2 {
4449 return Err(KclError::refactor(format!(
4450 "Parallel constraint must have at least 2 lines, got {}",
4451 parallel.lines.len()
4452 )));
4453 };
4454
4455 let sketch_id = sketch;
4456
4457 let line_asts = parallel
4458 .lines
4459 .iter()
4460 .map(|line_id| {
4461 let line_object = self
4462 .scene_graph
4463 .objects
4464 .get(line_id.0)
4465 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4466 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4467 let kind = line_object.kind.human_friendly_kind_with_article();
4468 return Err(KclError::refactor(format!(
4469 "This constraint only works on Segments, but you selected {kind}"
4470 )));
4471 };
4472 let Segment::Line(_) = line_segment else {
4473 let kind = line_segment.human_friendly_kind_with_article();
4474 return Err(KclError::refactor(format!(
4475 "Only lines can be made parallel, but you selected {kind}"
4476 )));
4477 };
4478
4479 self.line_id_to_ast_reference(*line_id, new_ast)
4480 })
4481 .collect::<Result<Vec<_>, _>>()?;
4482
4483 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4484 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4485 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4486 ast::ArrayExpression {
4487 elements: line_asts,
4488 digest: None,
4489 non_code_meta: Default::default(),
4490 },
4491 )))),
4492 arguments: Default::default(),
4493 digest: None,
4494 non_code_meta: Default::default(),
4495 })));
4496
4497 let (sketch_block_ref, _) = self.mutate_ast(
4498 new_ast,
4499 sketch_id,
4500 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4501 )?;
4502 Ok(sketch_block_ref)
4503 }
4504
4505 async fn add_perpendicular(
4506 &mut self,
4507 sketch: ObjectId,
4508 perpendicular: Perpendicular,
4509 new_ast: &mut ast::Node<ast::Program>,
4510 ) -> Result<AstNodeRef, KclError> {
4511 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4512 .await
4513 }
4514
4515 async fn add_lines_at_angle_constraint(
4516 &mut self,
4517 sketch: ObjectId,
4518 angle_kind: LinesAtAngleKind,
4519 lines: Vec<ObjectId>,
4520 new_ast: &mut ast::Node<ast::Program>,
4521 ) -> Result<AstNodeRef, KclError> {
4522 let &[line0_id, line1_id] = lines.as_slice() else {
4523 return Err(KclError::refactor(format!(
4524 "{} constraint must have exactly 2 lines, got {}",
4525 angle_kind.to_function_name(),
4526 lines.len()
4527 )));
4528 };
4529
4530 let sketch_id = sketch;
4531
4532 let line0_object = self
4534 .scene_graph
4535 .objects
4536 .get(line0_id.0)
4537 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4538 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4539 let kind = line0_object.kind.human_friendly_kind_with_article();
4540 return Err(KclError::refactor(format!(
4541 "This constraint only works on Segments, but you selected {kind}"
4542 )));
4543 };
4544 let Segment::Line(_) = line0_segment else {
4545 return Err(KclError::refactor(format!(
4546 "Only lines can be made {}, but you selected {}",
4547 angle_kind.to_function_name(),
4548 line0_segment.human_friendly_kind_with_article(),
4549 )));
4550 };
4551 let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4552
4553 let line1_object = self
4554 .scene_graph
4555 .objects
4556 .get(line1_id.0)
4557 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4558 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4559 let kind = line1_object.kind.human_friendly_kind_with_article();
4560 return Err(KclError::refactor(format!(
4561 "This constraint only works on Segments, but you selected {kind}"
4562 )));
4563 };
4564 let Segment::Line(_) = line1_segment else {
4565 return Err(KclError::refactor(format!(
4566 "Only lines can be made {}, but you selected {}",
4567 angle_kind.to_function_name(),
4568 line1_segment.human_friendly_kind_with_article(),
4569 )));
4570 };
4571 let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4572
4573 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4575 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4576 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4577 ast::ArrayExpression {
4578 elements: vec![line0_ast, line1_ast],
4579 digest: None,
4580 non_code_meta: Default::default(),
4581 },
4582 )))),
4583 arguments: Default::default(),
4584 digest: None,
4585 non_code_meta: Default::default(),
4586 })));
4587
4588 let (sketch_block_ref, _) = self.mutate_ast(
4590 new_ast,
4591 sketch_id,
4592 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4593 )?;
4594 Ok(sketch_block_ref)
4595 }
4596
4597 async fn add_vertical(
4598 &mut self,
4599 sketch: ObjectId,
4600 vertical: Vertical,
4601 new_ast: &mut ast::Node<ast::Program>,
4602 ) -> Result<AstNodeRef, KclError> {
4603 let sketch_id = sketch;
4604
4605 let first_arg_ast = match vertical {
4606 Vertical::Line { line } => {
4607 let line_object = self
4609 .scene_graph
4610 .objects
4611 .get(line.0)
4612 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4613 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4614 let kind = line_object.kind.human_friendly_kind_with_article();
4615 return Err(KclError::refactor(format!(
4616 "This constraint only works on Segments, but you selected {kind}"
4617 )));
4618 };
4619 let Segment::Line(_) = line_segment else {
4620 return Err(KclError::refactor(format!(
4621 "Only lines can be made vertical, but you selected {}",
4622 line_segment.human_friendly_kind_with_article()
4623 )));
4624 };
4625 self.line_id_to_ast_reference(line, new_ast)?
4626 }
4627 Vertical::Points { points } => {
4628 let point_asts = points
4629 .iter()
4630 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4631 .collect::<Result<Vec<_>, _>>()?;
4632 ast::ArrayExpression::new(point_asts).into()
4633 }
4634 };
4635 let vertical_ast = create_vertical_ast(first_arg_ast);
4637
4638 let (sketch_block_ref, _) = self.mutate_ast(
4640 new_ast,
4641 sketch_id,
4642 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4643 )?;
4644 Ok(sketch_block_ref)
4645 }
4646
4647 async fn execute_after_add_constraint(
4648 &mut self,
4649 ctx: &ExecutorContext,
4650 sketch_id: ObjectId,
4651 sketch_block_ref: AstNodeRef,
4652 new_ast: &mut ast::Node<ast::Program>,
4653 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4654 let new_source = source_from_ast(new_ast);
4656 let new_program = parse_frontend_mutation_source(
4658 &new_source,
4659 "Error parsing KCL source after adding constraint",
4660 "No AST produced after adding constraint",
4661 )?;
4662 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4663 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4664 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4665 )))
4666 })?;
4667
4668 let mut truncated_program = new_program.clone();
4671 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4672 .map_err(KclErrorWithOutputs::no_outputs)?;
4673
4674 let outcome = ctx
4676 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4677 .await?;
4678
4679 let new_object_ids = {
4680 let constraint_id = outcome
4682 .source_range_to_object
4683 .get(&constraint_node_ref.range)
4684 .copied()
4685 .ok_or_else(|| {
4686 KclErrorWithOutputs::from_error_outcome(
4687 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4688 outcome.clone(),
4689 )
4690 })?;
4691 vec![constraint_id]
4692 };
4693
4694 self.program = new_program;
4697
4698 let outcome = self.update_state_after_exec(outcome, true);
4700
4701 let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4702 let scene_graph_delta = SceneGraphDelta {
4703 new_graph: self.scene_graph_for_ui(),
4704 invalidates_ids: false,
4705 new_objects: new_object_ids,
4706 exec_outcome: outcome,
4707 };
4708 Ok((src_delta, scene_graph_delta))
4709 }
4710
4711 fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4712 let commit_failure = || {
4713 KclErrorWithOutputs::from_error_outcome(
4714 KclError::refactor(format!("Could not update KCL after {operation}.")),
4715 outcome.clone(),
4716 )
4717 };
4718
4719 let default_length_unit = self.default_length_unit();
4720 let mut settled_ast = self.program.ast.clone();
4721 let mut committed_solver_value = false;
4722 for (var_range, node_path, value) in &outcome.var_solutions {
4723 let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4724 return Err(commit_failure());
4725 };
4726 let new_value = match &lookup {
4727 Some(current_literal) => {
4728 if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4729 continue;
4730 }
4731 preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4732 }
4733 None => {
4734 Number {
4738 value: number_value_in_default_length_units(*value, default_length_unit),
4739 units: default_length_unit.into(),
4740 }
4741 }
4742 };
4743 committed_solver_value = true;
4744 let source_ref = SourceRef::Simple {
4745 range: *var_range,
4746 node_path: node_path.clone(),
4747 };
4748 mutate_ast_node_by_source_ref(
4749 &mut settled_ast,
4750 &source_ref,
4751 AstMutateCommand::EditVarInitialValue { value: new_value },
4752 )
4753 .map_err(|_| commit_failure())?;
4754 }
4755
4756 if !committed_solver_value {
4757 return Ok(SourceDelta {
4758 text: self.program.original_file_contents.clone(),
4759 });
4760 }
4761
4762 let settled_source = source_from_ast(&settled_ast);
4763 let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4764 if !errors.is_empty() {
4765 return Err(commit_failure());
4766 }
4767 let Some(settled_program) = settled_program else {
4768 return Err(commit_failure());
4769 };
4770
4771 self.program = settled_program;
4772
4773 Ok(SourceDelta { text: settled_source })
4774 }
4775
4776 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4778 if segment_ids_set.contains(&segment_id) {
4779 return true;
4780 }
4781
4782 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4783 return false;
4784 };
4785 let ObjectKind::Segment { segment } = &segment_object.kind else {
4786 return false;
4787 };
4788 let Segment::Point(point) = segment else {
4789 return false;
4790 };
4791
4792 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4793 }
4794
4795 fn remaining_constraint_segments(
4796 &self,
4797 segments: &[ConstraintSegment],
4798 segment_ids_set: &AhashIndexSet<ObjectId>,
4799 ) -> Vec<ConstraintSegment> {
4800 segments
4801 .iter()
4802 .copied()
4803 .filter(|segment| match segment {
4804 ConstraintSegment::Origin(_) => true,
4805 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4806 })
4807 .collect()
4808 }
4809
4810 fn find_referenced_constraints(
4811 &self,
4812 sketch_id: ObjectId,
4813 segment_ids_set: &AhashIndexSet<ObjectId>,
4814 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4815 let sketch_object = self
4817 .scene_graph
4818 .objects
4819 .get(sketch_id.0)
4820 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4821 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4822 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4823 };
4824 let segment_or_owner_matches = |segment_id: ObjectId| {
4825 if segment_ids_set.contains(&segment_id) {
4826 return true;
4827 }
4828 let segment_object = self.scene_graph.objects.get(segment_id.0);
4829 if let Some(obj) = segment_object
4830 && let ObjectKind::Segment { segment } = &obj.kind
4831 {
4832 match segment {
4833 Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4834 Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4835 _ => false,
4836 }
4837 } else {
4838 false
4839 }
4840 };
4841 let mut constraint_ids_set = AhashIndexSet::default();
4842 for constraint_id in &sketch.constraints {
4843 let constraint_object = self
4844 .scene_graph
4845 .objects
4846 .get(constraint_id.0)
4847 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4848 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4849 return Err(KclError::refactor(format!(
4850 "Object is not a constraint, it is {}",
4851 constraint_object.kind.human_friendly_kind_with_article()
4852 )));
4853 };
4854 let depends_on_segment = match constraint {
4855 Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4856 Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4857 Constraint::Fixed(fixed) => fixed
4858 .points
4859 .iter()
4860 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4861 Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4862 Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4863 Constraint::EqualRadius(equal_radius) => {
4864 equal_radius.input.iter().copied().any(segment_or_owner_matches)
4865 }
4866 Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4867 Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4868 Constraint::Horizontal(h) => match h {
4869 Horizontal::Line { line } => segment_or_owner_matches(*line),
4870 Horizontal::Points { points } => points.iter().any(|point| match point {
4871 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4872 ConstraintSegment::Origin(_) => false,
4873 }),
4874 },
4875 Constraint::Vertical(v) => match v {
4876 Vertical::Line { line } => segment_or_owner_matches(*line),
4877 Vertical::Points { points } => points.iter().any(|point| match point {
4878 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4879 ConstraintSegment::Origin(_) => false,
4880 }),
4881 },
4882 Constraint::LinesEqualLength(lines_equal_length) => {
4883 lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4884 }
4885 Constraint::Midpoint(midpoint) => {
4886 segment_or_owner_matches(midpoint.segment)
4887 || matches!(
4888 midpoint.point,
4889 ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4890 )
4891 }
4892 Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4893 Constraint::Perpendicular(perpendicular) => {
4894 perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4895 }
4896 Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4897 Constraint::Symmetric(symmetric) => {
4898 segment_or_owner_matches(symmetric.axis)
4899 || symmetric.input.iter().copied().any(segment_or_owner_matches)
4900 }
4901 Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4902 };
4903 if depends_on_segment {
4904 constraint_ids_set.insert(*constraint_id);
4905 }
4906 }
4907 Ok(constraint_ids_set)
4908 }
4909
4910 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4911 let mut outcome = outcome;
4912 self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4913 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4914
4915 if freedom_analysis_ran {
4916 self.point_freedom_cache.clear();
4919 for new_obj in &new_objects {
4920 if let ObjectKind::Segment {
4921 segment: crate::front::Segment::Point(point),
4922 } = &new_obj.kind
4923 {
4924 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4925 }
4926 }
4927 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4928 self.scene_graph.objects = new_objects;
4930 } else {
4931 for old_obj in &self.scene_graph.objects {
4934 if let ObjectKind::Segment {
4935 segment: crate::front::Segment::Point(point),
4936 } = &old_obj.kind
4937 {
4938 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4939 }
4940 }
4941
4942 let mut updated_objects = Vec::with_capacity(new_objects.len());
4944 for new_obj in new_objects {
4945 let mut obj = new_obj;
4946 if let ObjectKind::Segment {
4947 segment: crate::front::Segment::Point(point),
4948 } = &mut obj.kind
4949 {
4950 let new_freedom = point.freedom;
4951 match new_freedom {
4957 Freedom::Free => {
4958 match self.point_freedom_cache.get(&obj.id).copied() {
4959 Some(Freedom::Conflict) => {
4960 }
4963 Some(Freedom::Fixed) => {
4964 point.freedom = Freedom::Fixed;
4966 }
4967 Some(Freedom::Free) => {
4968 }
4970 None => {
4971 }
4973 }
4974 }
4975 Freedom::Fixed => {
4976 }
4978 Freedom::Conflict => {
4979 }
4981 }
4982 self.point_freedom_cache.insert(obj.id, point.freedom);
4984 }
4985 updated_objects.push(obj);
4986 }
4987
4988 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4989 self.scene_graph.objects = updated_objects;
4990 }
4991 outcome
4992 }
4993
4994 fn mutate_ast(
4995 &mut self,
4996 ast: &mut ast::Node<ast::Program>,
4997 object_id: ObjectId,
4998 command: AstMutateCommand,
4999 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5000 let sketch_object = self
5001 .scene_graph
5002 .objects
5003 .get(object_id.0)
5004 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5005 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5006 }
5007
5008 fn mutate_constraint_label_position(
5009 &mut self,
5010 ast: &mut ast::Node<ast::Program>,
5011 constraint_id: ObjectId,
5012 label_position: Point2d<Number>,
5013 ) -> Result<(), KclError> {
5014 let object = self
5015 .scene_graph
5016 .objects
5017 .get(constraint_id.0)
5018 .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5019 if !matches!(
5020 &object.kind,
5021 ObjectKind::Constraint {
5022 constraint: Constraint::Distance(_)
5023 | Constraint::HorizontalDistance(_)
5024 | Constraint::VerticalDistance(_)
5025 | Constraint::Radius(_)
5026 | Constraint::Diameter(_)
5027 | Constraint::Angle(_),
5028 }
5029 ) {
5030 return Err(KclError::refactor(format!(
5031 "Object does not support labelPosition: {constraint_id:?}"
5032 )));
5033 }
5034
5035 let label_position = to_ast_point2d_number(&label_position)
5036 .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5037 self.mutate_ast(
5038 ast,
5039 constraint_id,
5040 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5041 )?;
5042 Ok(())
5043 }
5044}
5045
5046fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5047 let sketch_object = scene_graph
5049 .objects
5050 .get(sketch_id.0)
5051 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5052 let ObjectKind::Sketch(_) = &sketch_object.kind else {
5053 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5054 };
5055 expect_single_node_ref(sketch_object)
5056}
5057
5058fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5059 match &object.source {
5060 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5061 range: *range,
5062 node_path: node_path.clone(),
5063 }),
5064 SourceRef::BackTrace { ranges } => {
5065 let [range] = ranges.as_slice() else {
5066 return Err(KclError::refactor(format!(
5067 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5068 ranges.len()
5069 )));
5070 };
5071 Ok(AstNodeRef {
5072 range: range.0,
5073 node_path: range.1.clone(),
5074 })
5075 }
5076 }
5077}
5078
5079fn only_sketch_block_from_range(
5082 ast: &mut ast::Node<ast::Program>,
5083 sketch_block_range: SourceRange,
5084 edit_kind: ChangeKind,
5085) -> Result<(), KclError> {
5086 let r1 = sketch_block_range;
5087 let matches_range = |r2: SourceRange| -> bool {
5088 match edit_kind {
5091 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5092 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5094 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5095 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5097 }
5098 };
5099 let mut found = false;
5100 for item in ast.body.iter_mut() {
5101 match item {
5102 ast::BodyItem::ImportStatement(_) => {}
5103 ast::BodyItem::ExpressionStatement(node) => {
5104 if matches_range(SourceRange::from(&*node))
5105 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5106 {
5107 sketch_block.is_being_edited = true;
5108 found = true;
5109 break;
5110 }
5111 }
5112 ast::BodyItem::VariableDeclaration(node) => {
5113 if matches_range(SourceRange::from(&node.declaration.init))
5114 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5115 {
5116 sketch_block.is_being_edited = true;
5117 found = true;
5118 break;
5119 }
5120 }
5121 ast::BodyItem::TypeDeclaration(_) => {}
5122 ast::BodyItem::ReturnStatement(node) => {
5123 if matches_range(SourceRange::from(&node.argument))
5124 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5125 {
5126 sketch_block.is_being_edited = true;
5127 found = true;
5128 break;
5129 }
5130 }
5131 }
5132 }
5133 if !found {
5134 return Err(KclError::refactor(format!(
5135 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5136 )));
5137 }
5138
5139 Ok(())
5140}
5141
5142fn only_sketch_block(
5143 ast: &mut ast::Node<ast::Program>,
5144 sketch_block_ref: &AstNodeRef,
5145 edit_kind: ChangeKind,
5146) -> Result<(), KclError> {
5147 let Some(target_node_path) = &sketch_block_ref.node_path else {
5148 #[cfg(target_arch = "wasm32")]
5149 web_sys::console::warn_1(
5150 &format!(
5151 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5152 sketch_block_ref
5153 )
5154 .into(),
5155 );
5156 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5157 };
5158 struct MarkSketchBlockBeingEdited<'a> {
5159 target_node_path: &'a ast::NodePath,
5160 }
5161
5162 impl Visitor for MarkSketchBlockBeingEdited<'_> {
5163 type Break = ();
5164 type Continue = ();
5165
5166 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5167 if let NodeMut::SketchBlock(sketch_block) = node
5168 && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5169 {
5170 sketch_block.is_being_edited = true;
5171 return TraversalReturn::new_break(());
5172 }
5173 TraversalReturn::new_continue(())
5174 }
5175
5176 fn finish(&mut self, _node: NodeMut<'_>) {}
5177 }
5178
5179 let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5180 let found = dfs_mut(ast, &mut marker).is_break();
5181 if !found {
5182 return Err(KclError::refactor(format!(
5183 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5184 )));
5185 }
5186
5187 Ok(())
5188}
5189
5190fn sketch_on_ast_expr(
5191 ast: &mut ast::Node<ast::Program>,
5192 scene_graph: &SceneGraph,
5193 solid_references: &HashMap<Uuid, SolidAstReference>,
5194 on: &Plane,
5195) -> Result<ast::Expr, KclError> {
5196 match on {
5197 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5198 Plane::Object(object_id) => {
5199 let on_object = scene_graph
5200 .objects
5201 .get(object_id.0)
5202 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5203 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5204 return Ok(face_expr);
5205 }
5206 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5207 }
5208 Plane::PrimitiveFace(face) => {
5209 let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5210 KclError::refactor(format!(
5211 "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5212 face.solid_id
5213 ))
5214 })?;
5215 let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5216 Ok(create_face_of_ast(solid_expr, face_id_expr))
5217 }
5218 }
5219}
5220
5221fn solid_references_from_variables(
5222 ast: &ast::Node<ast::Program>,
5223 variables: &IndexMap<String, KclValueView>,
5224) -> HashMap<Uuid, SolidAstReference> {
5225 let mut references = HashMap::new();
5226
5227 for item in &ast.body {
5230 let ast::BodyItem::VariableDeclaration(declaration) = item else {
5231 continue;
5232 };
5233 let name = &declaration.declaration.id.name;
5234 let Some(value) = variables.get(name) else {
5235 continue;
5236 };
5237
5238 match value {
5239 KclValueView::Solid { value } => {
5240 references.insert(
5241 value.id,
5242 SolidAstReference {
5243 variable_name: name.clone(),
5244 output_index: None,
5245 },
5246 );
5247 }
5248 KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5249 for (output_index, entry) in value.iter().enumerate() {
5250 if let KclValueView::Solid { value } = entry {
5251 references.insert(
5252 value.id,
5253 SolidAstReference {
5254 variable_name: name.clone(),
5255 output_index: Some(output_index),
5256 },
5257 );
5258 }
5259 }
5260 }
5261 _ => {}
5262 }
5263 }
5264
5265 references
5266}
5267
5268fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5269 let reference = solid_references.get(&solid_id)?;
5270 let solid_expr = ast_name_expr(reference.variable_name.clone());
5271 Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5272}
5273
5274fn sketch_face_of_scene_object_ast_expr(
5275 ast: &mut ast::Node<ast::Program>,
5276 on_object: &crate::front::Object,
5277) -> Result<Option<ast::Expr>, KclError> {
5278 match &on_object.kind {
5279 ObjectKind::Wall(wall) => {
5280 let solid_ref = get_or_insert_ast_reference(
5281 ast,
5282 &source_ref_from_source_ref_range(&wall.source.solid),
5283 "solid",
5284 None,
5285 )?;
5286 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5287 return Err(KclError::refactor(format!(
5288 "Could not resolve solid reference for selected wall: artifact_id={:?}",
5289 on_object.artifact_id
5290 )));
5291 };
5292 let solid_expr = indexed_solid_expr_for_sweep_output(
5293 ast_name_expr(solid_name_expr.name.name.clone()),
5294 wall.solid_output_index,
5295 );
5296 let sweep_ref = get_or_insert_ast_reference(
5297 ast,
5298 &source_ref_from_source_ref_range(&wall.source.sweep),
5299 "solid",
5300 None,
5301 )?;
5302 let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5303 return Err(KclError::refactor(format!(
5304 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5305 on_object.artifact_id
5306 )));
5307 };
5308 let sweep_name = sweep_name_expr.name.name.clone();
5309 let segment_ref = get_or_insert_ast_reference(
5310 ast,
5311 &source_ref_from_source_ref_range(&wall.source.segment),
5312 LINE_VARIABLE,
5313 None,
5314 )?;
5315
5316 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5317 wall.source
5318 .path
5319 .as_ref()
5320 .and_then(|path_source| region_name_from_path_source(ast, path_source))
5321 }) {
5322 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5323 return Err(KclError::refactor(format!(
5324 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5325 on_object.artifact_id
5326 )));
5327 };
5328 create_member_expression(
5329 create_member_expression(ast_name_expr(region_name), "tags"),
5330 &segment_name_expr.name.name,
5331 )
5332 } else {
5333 segment_ref
5334 };
5335
5336 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5337 }
5338 ObjectKind::Cap(cap) => {
5339 let solid_ref =
5340 get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5341 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5342 return Err(KclError::refactor(format!(
5343 "Could not resolve solid reference for selected cap: artifact_id={:?}",
5344 on_object.artifact_id
5345 )));
5346 };
5347 let solid_expr = indexed_solid_expr_for_sweep_output(
5348 ast_name_expr(solid_name_expr.name.name.clone()),
5349 cap.solid_output_index,
5350 );
5351 let face_expr = match cap.kind {
5353 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5354 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5355 };
5356
5357 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5358 }
5359 _ => Ok(None),
5360 }
5361}
5362
5363fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5364 match solid_output_index {
5365 Some(output_index) => create_index_expression(solid_expr, output_index),
5366 None => solid_expr,
5367 }
5368}
5369
5370fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5371 SourceRef::Simple {
5372 range: source.range,
5373 node_path: source.node_path.clone(),
5374 }
5375}
5376
5377fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5378 let source_ref = source_ref_from_source_ref_range(path_source);
5379 let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5380 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5381 return None;
5382 };
5383 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5384 return None;
5385 };
5386 if region_call.callee.name.name != "region" {
5387 return None;
5388 }
5389 Some(candidate)
5390}
5391
5392fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5393 let mut current_id = source_id;
5394 let mut current_composite = None;
5395 let mut visited = HashSet::new();
5396
5397 while visited.insert(current_id) {
5398 let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5399
5400 let Some(composite_id) = next_composite_id else {
5401 break;
5402 };
5403 let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5404 break;
5405 };
5406
5407 current_id = composite.id;
5408 current_composite = Some(composite);
5409
5410 if !composite.consumed {
5411 break;
5412 }
5413 }
5414
5415 current_composite.map(|composite| &composite.code_ref)
5416}
5417
5418fn downstream_composite_id_for_solid_source(
5419 artifact_graph: &ArtifactGraph,
5420 source_id: ArtifactId,
5421) -> Option<ArtifactId> {
5422 if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5424 && let Some(composite_id) = path.composite_solid_id
5425 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5426 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5427 {
5428 return Some(composite_id);
5429 }
5430
5431 for artifact in artifact_graph.values() {
5433 if let Artifact::Path(path) = artifact
5434 && path.sweep_id == Some(source_id)
5435 && let Some(composite_id) = path.composite_solid_id
5436 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5437 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5438 {
5439 return Some(composite_id);
5440 }
5441 }
5442
5443 artifact_graph.values().find_map(|artifact| {
5445 let Artifact::CompositeSolid(composite) = artifact else {
5446 return None;
5447 };
5448 composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5449 })
5450}
5451
5452fn composite_contains_path_input(
5453 solid_ids: &[ArtifactId],
5454 tool_ids: &[ArtifactId],
5455 path_id: ArtifactId,
5456 solid2d_id: Option<ArtifactId>,
5457) -> bool {
5458 composite_contains_input(solid_ids, tool_ids, path_id)
5459 || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5460}
5461
5462fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5463 solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5464}
5465
5466fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5467 let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5468 SourceRefRange {
5469 range: code_ref.range,
5470 node_path,
5471 }
5472}
5473
5474fn solid_output_index_for_sweep(
5475 artifact_graph: &ArtifactGraph,
5476 sweep_id: ArtifactId,
5477 sweep_code_ref: &CodeRef,
5478) -> Option<usize> {
5479 if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5482 return None;
5483 }
5484
5485 let sibling_sweeps = artifact_graph
5486 .values()
5487 .filter_map(|artifact| match artifact {
5488 Artifact::Sweep(sweep)
5489 if sweep.code_ref.range == sweep_code_ref.range
5490 && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5491 {
5492 Some(sweep)
5493 }
5494 _ => None,
5495 })
5496 .collect::<Vec<_>>();
5497
5498 if sibling_sweeps.len() <= 1 {
5499 return None;
5500 }
5501
5502 sibling_sweeps
5503 .iter()
5504 .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5505}
5506
5507fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5508 let mut existing_artifact_ids = scene_objects
5509 .iter()
5510 .map(|object| object.artifact_id)
5511 .collect::<HashSet<_>>();
5512
5513 for artifact in artifact_graph.values() {
5514 match artifact {
5515 Artifact::Wall(wall) => {
5516 if existing_artifact_ids.contains(&wall.id) {
5517 continue;
5518 }
5519
5520 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5521 Artifact::Segment(segment) => Some(segment),
5522 _ => None,
5523 }) else {
5524 continue;
5525 };
5526 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5527 Artifact::Sweep(sweep) => Some(sweep),
5528 _ => None,
5529 }) else {
5530 continue;
5531 };
5532 let source_segment = segment
5533 .original_seg_id
5534 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5535 .and_then(|artifact| match artifact {
5536 Artifact::Segment(segment) => Some(segment),
5537 _ => None,
5538 })
5539 .unwrap_or(segment);
5540 let solid_code_ref =
5541 downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5542 let path_code_ref = artifact_graph
5543 .get(&segment.path_id)
5544 .or_else(|| artifact_graph.get(&sweep.path_id))
5545 .and_then(|artifact| match artifact {
5546 Artifact::Path(path) => Some(&path.code_ref),
5547 _ => None,
5548 });
5549 let source = WallSource {
5550 solid: code_ref_source_ref_range(solid_code_ref),
5551 sweep: code_ref_source_ref_range(&sweep.code_ref),
5552 path: path_code_ref.map(code_ref_source_ref_range),
5553 segment: code_ref_source_ref_range(&source_segment.code_ref),
5554 };
5555 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5556 && solid_code_ref.node_path == sweep.code_ref.node_path)
5557 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5558 .flatten();
5559 let object_source = source_ref_from_source_ref_range(&source.solid);
5560 let id = ObjectId(scene_objects.len());
5561 scene_objects.push(crate::front::Object {
5562 id,
5563 kind: ObjectKind::Wall(crate::frontend::api::Wall {
5564 id,
5565 source,
5566 solid_output_index,
5567 }),
5568 label: Default::default(),
5569 comments: Default::default(),
5570 artifact_id: wall.id,
5571 source: object_source,
5572 });
5573 existing_artifact_ids.insert(wall.id);
5574 }
5575 Artifact::Cap(cap) => {
5576 if existing_artifact_ids.contains(&cap.id) {
5577 continue;
5578 }
5579
5580 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5581 Artifact::Sweep(sweep) => Some(sweep),
5582 _ => None,
5583 }) else {
5584 continue;
5585 };
5586 let id = ObjectId(scene_objects.len());
5587 let kind = match cap.sub_type {
5588 CapSubType::Start => crate::frontend::api::CapKind::Start,
5589 CapSubType::End => crate::frontend::api::CapKind::End,
5590 };
5591 let solid_code_ref =
5592 downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5593 let source = CapSource {
5594 solid: code_ref_source_ref_range(solid_code_ref),
5595 sweep: code_ref_source_ref_range(&sweep.code_ref),
5596 };
5597 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5598 && solid_code_ref.node_path == sweep.code_ref.node_path)
5599 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5600 .flatten();
5601 let object_source = source_ref_from_source_ref_range(&source.solid);
5602 scene_objects.push(crate::front::Object {
5603 id,
5604 kind: ObjectKind::Cap(crate::frontend::api::Cap {
5605 id,
5606 kind,
5607 source,
5608 solid_output_index,
5609 }),
5610 label: Default::default(),
5611 comments: Default::default(),
5612 artifact_id: cap.id,
5613 source: object_source,
5614 });
5615 existing_artifact_ids.insert(cap.id);
5616 }
5617 _ => {}
5618 }
5619 }
5620}
5621
5622fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5623 use crate::engine::PlaneName;
5624
5625 match name {
5626 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5627 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5628 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5629 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5630 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5631 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5632 }
5633}
5634
5635fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5636 ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5637 ast::UnaryOperator::Neg,
5638 ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5639 )))
5640}
5641
5642fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5643 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5644 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5645 unlabeled: Some(solid_expr),
5646 arguments: vec![ast::LabeledArg {
5647 label: Some(ast::Identifier::new("face")),
5648 arg: face_expr,
5649 }],
5650 digest: None,
5651 non_code_meta: Default::default(),
5652 })))
5653}
5654
5655fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5656 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5657 callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5658 unlabeled: Some(solid_expr),
5659 arguments: vec![ast::LabeledArg {
5660 label: Some(ast::Identifier::new("index")),
5661 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5662 ast::NumericLiteral {
5663 value: index as f64,
5664 suffix: NumericSuffix::None,
5665 raw: index.to_string(),
5666 digest: None,
5667 },
5668 )))),
5669 }],
5670 digest: None,
5671 non_code_meta: Default::default(),
5672 })))
5673}
5674
5675fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5676 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5677 return None;
5678 };
5679 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5680 return None;
5681 };
5682 if !matches!(
5683 sweep_call.callee.name.name.as_str(),
5684 "extrude" | "revolve" | "sweep" | "loft"
5685 ) {
5686 return None;
5687 }
5688 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5689 return None;
5690 };
5691 let candidate = region_name_expr.name.name.clone();
5692 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5693 return None;
5694 };
5695 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5696 return None;
5697 };
5698 if region_call.callee.name.name != "region" {
5699 return None;
5700 }
5701 Some(candidate)
5702}
5703
5704fn get_or_insert_ast_reference(
5711 ast: &mut ast::Node<ast::Program>,
5712 source_ref: &SourceRef,
5713 prefix: &str,
5714 property: Option<&str>,
5715) -> Result<ast::Expr, KclError> {
5716 let command = AstMutateCommand::AddVariableDeclaration {
5717 prefix: prefix.to_owned(),
5718 };
5719 let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5720 Ok((_, ret)) => ret,
5721 Err(err) => {
5722 if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5723 AstMutateCommandReturn::Name(var_name)
5724 } else {
5725 return Err(err);
5726 }
5727 }
5728 };
5729 let AstMutateCommandReturn::Name(var_name) = ret else {
5730 return Err(KclError::refactor(
5731 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5732 ));
5733 };
5734 let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5735 let Some(property) = property else {
5736 return Ok(var_expr);
5738 };
5739
5740 Ok(create_member_expression(var_expr, property))
5741}
5742
5743fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5744 let source_range = match source_ref {
5745 SourceRef::Simple { range, .. } => *range,
5746 SourceRef::BackTrace { ranges } => {
5747 let [range] = ranges.as_slice() else {
5748 return None;
5749 };
5750 range.0
5751 }
5752 };
5753 ast.body.iter().find_map(|item| {
5754 let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5755 return None;
5756 };
5757 let init_range = SourceRange::from(&var_decl.declaration.init);
5758 let source_is_inside_init = init_range.module_id() == source_range.module_id()
5759 && init_range.start() <= source_range.start()
5760 && source_range.end() <= init_range.end();
5761 if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5762 && init_range != source_range
5763 && source_is_inside_init
5764 {
5765 return None;
5766 }
5767 source_is_inside_init.then(|| var_decl.name().to_owned())
5768 })
5769}
5770
5771fn mutate_ast_node_by_source_ref(
5772 ast: &mut ast::Node<ast::Program>,
5773 source_ref: &SourceRef,
5774 command: AstMutateCommand,
5775) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5776 let (source_range, node_path) = match source_ref {
5777 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5778 SourceRef::BackTrace { ranges } => {
5779 let [range] = ranges.as_slice() else {
5780 return Err(KclError::refactor(format!(
5781 "Expected single source ref, got {}; ranges={ranges:#?}",
5782 ranges.len(),
5783 )));
5784 };
5785 (range.0, range.1.clone())
5786 }
5787 };
5788 let mut context = AstMutateContext {
5789 source_range,
5790 node_path,
5791 command,
5792 defined_names_stack: Default::default(),
5793 };
5794 let control = dfs_mut(ast, &mut context);
5795 match control {
5796 ControlFlow::Continue(_) => Err(KclError::refactor(
5797 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5798 )),
5799 ControlFlow::Break(break_value) => break_value,
5800 }
5801}
5802
5803#[derive(Debug)]
5804struct AstMutateContext {
5805 source_range: SourceRange,
5806 node_path: Option<ast::NodePath>,
5807 command: AstMutateCommand,
5808 defined_names_stack: Vec<HashSet<String>>,
5809}
5810
5811#[derive(Debug)]
5812#[allow(clippy::large_enum_variant)]
5813enum AstMutateCommand {
5814 AddSketchBlockExprStmt {
5816 expr: ast::Expr,
5817 },
5818 AddSketchBlockVarDecl {
5820 prefix: String,
5821 expr: ast::Expr,
5822 },
5823 AddVariableDeclaration {
5824 prefix: String,
5825 },
5826 EditPoint {
5827 at: ast::Expr,
5828 },
5829 EditLine {
5830 start: ast::Expr,
5831 end: ast::Expr,
5832 construction: Option<bool>,
5833 },
5834 EditArc {
5835 start: ast::Expr,
5836 end: ast::Expr,
5837 center: ast::Expr,
5838 direction: Option<ArcDirection>,
5839 construction: Option<bool>,
5840 },
5841 EditCircle {
5842 start: ast::Expr,
5843 center: ast::Expr,
5844 construction: Option<bool>,
5845 },
5846 EditControlPointSpline {
5847 points: ast::Expr,
5848 construction: Option<bool>,
5849 },
5850 EditConstraintValue {
5851 value: ast::BinaryPart,
5852 },
5853 EditAngleConstraint {
5854 call: ast::BinaryPart,
5855 value: ast::BinaryPart,
5856 },
5857 EditDistanceConstraint {
5858 call: ast::BinaryPart,
5859 value: ast::BinaryPart,
5860 },
5861 EditDistanceConstraintLabelPosition {
5862 label_position: ast::Expr,
5863 },
5864 EditCallUnlabeled {
5865 arg: ast::Expr,
5866 },
5867 EditVarInitialValue {
5868 value: Number,
5869 },
5870 DeleteNode,
5871}
5872
5873impl AstMutateCommand {
5874 fn needs_defined_names_stack(&self) -> bool {
5875 matches!(
5876 self,
5877 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5878 )
5879 }
5880}
5881
5882#[derive(Debug)]
5883enum AstMutateCommandReturn {
5884 None,
5885 Name(String),
5886}
5887
5888#[derive(Debug, Clone)]
5889struct AstNodeRef {
5890 range: SourceRange,
5891 node_path: Option<ast::NodePath>,
5892}
5893
5894impl<T> From<&ast::Node<T>> for AstNodeRef {
5895 fn from(value: &ast::Node<T>) -> Self {
5896 AstNodeRef {
5897 range: value.into(),
5898 node_path: value.node_path.clone(),
5899 }
5900 }
5901}
5902
5903impl From<&ast::BodyItem> for AstNodeRef {
5904 fn from(value: &ast::BodyItem) -> Self {
5905 match value {
5906 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5907 range: node.into(),
5908 node_path: node.node_path.clone(),
5909 },
5910 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5911 range: node.into(),
5912 node_path: node.node_path.clone(),
5913 },
5914 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5915 range: node.into(),
5916 node_path: node.node_path.clone(),
5917 },
5918 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5919 range: node.into(),
5920 node_path: node.node_path.clone(),
5921 },
5922 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5923 range: node.into(),
5924 node_path: node.node_path.clone(),
5925 },
5926 }
5927 }
5928}
5929
5930impl From<&ast::Expr> for AstNodeRef {
5931 fn from(value: &ast::Expr) -> Self {
5932 AstNodeRef {
5933 range: SourceRange::from(value),
5934 node_path: value.node_path().cloned(),
5935 }
5936 }
5937}
5938
5939impl From<&AstMutateContext> for AstNodeRef {
5940 fn from(value: &AstMutateContext) -> Self {
5941 AstNodeRef {
5942 range: value.source_range,
5943 node_path: value.node_path.clone(),
5944 }
5945 }
5946}
5947
5948impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5949 type Error = crate::walk::AstNodeError;
5950
5951 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5952 Ok(AstNodeRef {
5953 range: SourceRange::try_from(value)?,
5954 node_path: value.try_into()?,
5955 })
5956 }
5957}
5958
5959impl From<AstNodeRef> for SourceRange {
5960 fn from(value: AstNodeRef) -> Self {
5961 value.range
5962 }
5963}
5964
5965impl Visitor for AstMutateContext {
5966 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5967 type Continue = ();
5968
5969 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5970 filter_and_process(self, node)
5971 }
5972
5973 fn finish(&mut self, node: NodeMut<'_>) {
5974 match &node {
5975 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5976 self.defined_names_stack.pop();
5977 }
5978 _ => {}
5979 }
5980 }
5981}
5982
5983fn filter_and_process(
5984 ctx: &mut AstMutateContext,
5985 node: NodeMut,
5986) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5987 let Ok(node_range) = SourceRange::try_from(&node) else {
5988 return TraversalReturn::new_continue(());
5990 };
5991 if let NodeMut::VariableDeclaration(var_decl) = &node {
5996 let expr_range = SourceRange::from(&var_decl.declaration.init);
5997 let expr_node_path = var_decl.declaration.init.node_path();
5998 if source_ref_matches(ctx, expr_range, expr_node_path) {
5999 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6000 return TraversalReturn::new_break(Ok((
6003 AstNodeRef::from(&**var_decl),
6004 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6005 )));
6006 }
6007 if let AstMutateCommand::DeleteNode = &ctx.command {
6008 return TraversalReturn {
6011 mutate_body_item: MutateBodyItem::Delete,
6012 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6013 };
6014 }
6015 }
6016 }
6017 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6020 let expr_range = SourceRange::from(&expr_stmt.expression);
6021 let expr_node_path = expr_stmt.expression.node_path();
6022 if source_ref_matches(ctx, expr_range, expr_node_path) {
6023 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6024 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6027 return TraversalReturn::new_continue(());
6028 };
6029 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6030 }
6031 if let AstMutateCommand::DeleteNode = &ctx.command {
6032 return TraversalReturn {
6035 mutate_body_item: MutateBodyItem::Delete,
6036 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6037 };
6038 }
6039 }
6040 }
6041
6042 if ctx.command.needs_defined_names_stack() {
6043 if let NodeMut::Program(program) = &node {
6044 ctx.defined_names_stack.push(find_defined_names(*program));
6045 } else if let NodeMut::SketchBlock(block) = &node {
6046 ctx.defined_names_stack.push(find_defined_names(&block.body));
6047 }
6048 }
6049
6050 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6052 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6053 return TraversalReturn::new_continue(());
6054 }
6055 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6056 return TraversalReturn::new_continue(());
6057 };
6058 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6059}
6060
6061fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6062 match &ctx.node_path {
6063 Some(target) => Some(target) == node_path,
6064 None => node_range == ctx.source_range,
6065 }
6066}
6067
6068fn is_angle_constraint_call_name(name: &str) -> bool {
6069 matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6070}
6071
6072fn is_distance_constraint_call_name(name: &str) -> bool {
6073 matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6074}
6075
6076fn is_constraint_call_name(name: &str) -> bool {
6077 matches!(
6078 name,
6079 DISTANCE_FN
6080 | HORIZONTAL_DISTANCE_FN
6081 | VERTICAL_DISTANCE_FN
6082 | RADIUS_FN
6083 | DIAMETER_FN
6084 | ANGLE_FN
6085 | ANGLE_DIMENSION_FN
6086 )
6087}
6088
6089fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6090 if let ast::BinaryPart::CallExpressionKw(call) = part
6091 && is_constraint_call_name(call.callee.name.name.as_str())
6092 {
6093 Some(call)
6094 } else {
6095 None
6096 }
6097}
6098
6099fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6100 match &ctx.command {
6101 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6102 if let NodeMut::SketchBlock(sketch_block) = node {
6103 sketch_block
6104 .body
6105 .items
6106 .push(ast::BodyItem::ExpressionStatement(ast::Node {
6107 inner: ast::ExpressionStatement {
6108 expression: expr.clone(),
6109 digest: None,
6110 },
6111 start: Default::default(),
6112 end: Default::default(),
6113 module_id: Default::default(),
6114 node_path: None,
6115 outer_attrs: Default::default(),
6116 pre_comments: Default::default(),
6117 comment_start: Default::default(),
6118 }));
6119 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6120 }
6121 }
6122 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6123 if let NodeMut::SketchBlock(sketch_block) = node {
6124 let empty_defined_names = HashSet::new();
6125 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6126 let Ok(name) = next_free_name(prefix, defined_names) else {
6127 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6128 };
6129 sketch_block
6130 .body
6131 .items
6132 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6133 ast::VariableDeclaration::new(
6134 ast::VariableDeclarator::new(&name, expr.clone()),
6135 ast::ItemVisibility::Default,
6136 ast::VariableKind::Const,
6137 ),
6138 ))));
6139 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6140 }
6141 }
6142 AstMutateCommand::AddVariableDeclaration { prefix } => {
6143 if let NodeMut::VariableDeclaration(inner) = node {
6144 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6145 }
6146 if let NodeMut::ExpressionStatement(expr_stmt) = node {
6147 let empty_defined_names = HashSet::new();
6148 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6149 let Ok(name) = next_free_name(prefix, defined_names) else {
6150 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6152 };
6153 let mutate_node =
6154 ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6155 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6156 ast::ItemVisibility::Default,
6157 ast::VariableKind::Const,
6158 ))));
6159 return TraversalReturn {
6160 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6161 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6162 };
6163 }
6164 }
6165 AstMutateCommand::EditPoint { at } => {
6166 if let NodeMut::CallExpressionKw(call) = node {
6167 if call.callee.name.name != POINT_FN {
6168 return TraversalReturn::new_continue(());
6169 }
6170 for labeled_arg in &mut call.arguments {
6172 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6173 labeled_arg.arg = at.clone();
6174 }
6175 }
6176 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6177 }
6178 }
6179 AstMutateCommand::EditLine {
6180 start,
6181 end,
6182 construction,
6183 } => {
6184 if let NodeMut::CallExpressionKw(call) = node {
6185 if call.callee.name.name != LINE_FN {
6186 return TraversalReturn::new_continue(());
6187 }
6188 for labeled_arg in &mut call.arguments {
6190 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6191 labeled_arg.arg = start.clone();
6192 }
6193 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6194 labeled_arg.arg = end.clone();
6195 }
6196 }
6197 if let Some(construction_value) = construction {
6199 let construction_exists = call
6200 .arguments
6201 .iter()
6202 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6203 if *construction_value {
6204 if construction_exists {
6206 for labeled_arg in &mut call.arguments {
6208 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6209 labeled_arg.arg =
6210 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6211 value: ast::LiteralValue::Bool(true),
6212 raw: "true".to_string(),
6213 digest: None,
6214 })));
6215 }
6216 }
6217 } else {
6218 call.arguments.push(ast::LabeledArg {
6220 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6221 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6222 value: ast::LiteralValue::Bool(true),
6223 raw: "true".to_string(),
6224 digest: None,
6225 }))),
6226 });
6227 }
6228 } else {
6229 call.arguments
6231 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6232 }
6233 }
6234 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6235 }
6236 }
6237 AstMutateCommand::EditArc {
6238 start,
6239 end,
6240 center,
6241 direction,
6242 construction,
6243 } => {
6244 if let NodeMut::CallExpressionKw(call) = node {
6245 if call.callee.name.name != ARC_FN {
6246 return TraversalReturn::new_continue(());
6247 }
6248 for labeled_arg in &mut call.arguments {
6250 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6251 labeled_arg.arg = start.clone();
6252 }
6253 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6254 labeled_arg.arg = end.clone();
6255 }
6256 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6257 labeled_arg.arg = center.clone();
6258 }
6259 }
6260 if let Some(direction_value) = direction {
6262 let direction_exists = call
6263 .arguments
6264 .iter()
6265 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6266 if direction_value.is_clockwise() {
6267 let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6268 if direction_exists {
6269 for labeled_arg in &mut call.arguments {
6271 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6272 labeled_arg.arg = direction_ast.clone();
6273 }
6274 }
6275 } else {
6276 call.arguments.push(ast::LabeledArg {
6278 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6279 arg: direction_ast,
6280 });
6281 }
6282 } else {
6283 call.arguments
6286 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6287 }
6288 }
6289 if let Some(construction_value) = construction {
6291 let construction_exists = call
6292 .arguments
6293 .iter()
6294 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6295 if *construction_value {
6296 if construction_exists {
6298 for labeled_arg in &mut call.arguments {
6300 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6301 labeled_arg.arg =
6302 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6303 value: ast::LiteralValue::Bool(true),
6304 raw: "true".to_string(),
6305 digest: None,
6306 })));
6307 }
6308 }
6309 } else {
6310 call.arguments.push(ast::LabeledArg {
6312 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6313 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6314 value: ast::LiteralValue::Bool(true),
6315 raw: "true".to_string(),
6316 digest: None,
6317 }))),
6318 });
6319 }
6320 } else {
6321 call.arguments
6323 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6324 }
6325 }
6326 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6327 }
6328 }
6329 AstMutateCommand::EditCircle {
6330 start,
6331 center,
6332 construction,
6333 } => {
6334 if let NodeMut::CallExpressionKw(call) = node {
6335 if call.callee.name.name != CIRCLE_FN {
6336 return TraversalReturn::new_continue(());
6337 }
6338 for labeled_arg in &mut call.arguments {
6340 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6341 labeled_arg.arg = start.clone();
6342 }
6343 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6344 labeled_arg.arg = center.clone();
6345 }
6346 }
6347 if let Some(construction_value) = construction {
6349 let construction_exists = call
6350 .arguments
6351 .iter()
6352 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6353 if *construction_value {
6354 if construction_exists {
6355 for labeled_arg in &mut call.arguments {
6357 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6358 labeled_arg.arg =
6359 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6360 value: ast::LiteralValue::Bool(true),
6361 raw: "true".to_string(),
6362 digest: None,
6363 })));
6364 }
6365 }
6366 } else {
6367 call.arguments.push(ast::LabeledArg {
6369 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6370 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6371 value: ast::LiteralValue::Bool(true),
6372 raw: "true".to_string(),
6373 digest: None,
6374 }))),
6375 });
6376 }
6377 } else {
6378 call.arguments
6380 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6381 }
6382 }
6383 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6384 }
6385 }
6386 AstMutateCommand::EditControlPointSpline { points, construction } => {
6387 if let NodeMut::CallExpressionKw(call) = node {
6388 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6389 return TraversalReturn::new_continue(());
6390 }
6391 for labeled_arg in &mut call.arguments {
6392 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6393 {
6394 labeled_arg.arg = points.clone();
6395 }
6396 }
6397 if let Some(construction_value) = construction {
6399 let construction_exists = call
6400 .arguments
6401 .iter()
6402 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6403 if *construction_value {
6404 if construction_exists {
6405 for labeled_arg in &mut call.arguments {
6406 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6407 labeled_arg.arg =
6408 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6409 value: ast::LiteralValue::Bool(true),
6410 raw: "true".to_string(),
6411 digest: None,
6412 })));
6413 }
6414 }
6415 } else {
6416 call.arguments.push(ast::LabeledArg {
6417 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6418 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6419 value: ast::LiteralValue::Bool(true),
6420 raw: "true".to_string(),
6421 digest: None,
6422 }))),
6423 });
6424 }
6425 } else {
6426 call.arguments
6427 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6428 }
6429 }
6430 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6431 }
6432 }
6433 AstMutateCommand::EditConstraintValue { value } => {
6434 if let NodeMut::BinaryExpression(binary_expr) = node {
6435 let left_is_constraint = matches!(
6436 &binary_expr.left,
6437 ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6438 );
6439 if left_is_constraint {
6440 binary_expr.right = value.clone();
6441 } else {
6442 binary_expr.left = value.clone();
6443 }
6444
6445 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6446 }
6447 }
6448 AstMutateCommand::EditAngleConstraint { call, value } => {
6449 if let NodeMut::BinaryExpression(binary_expr) = node {
6450 let left_is_angle = matches!(
6451 &binary_expr.left,
6452 ast::BinaryPart::CallExpressionKw(existing_call)
6453 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6454 );
6455 let right_is_angle = matches!(
6456 &binary_expr.right,
6457 ast::BinaryPart::CallExpressionKw(existing_call)
6458 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6459 );
6460
6461 match (left_is_angle, right_is_angle) {
6462 (true, _) => {
6463 binary_expr.left = call.clone();
6464 binary_expr.right = value.clone();
6465 }
6466 (false, true) => {
6467 binary_expr.left = value.clone();
6468 binary_expr.right = call.clone();
6469 }
6470 (false, false) => return TraversalReturn::new_continue(()),
6471 }
6472
6473 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6474 }
6475 }
6476 AstMutateCommand::EditDistanceConstraint { call, value } => {
6477 if let NodeMut::BinaryExpression(binary_expr) = node {
6478 let left_is_distance = matches!(
6479 &binary_expr.left,
6480 ast::BinaryPart::CallExpressionKw(existing_call)
6481 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6482 );
6483 let right_is_distance = matches!(
6484 &binary_expr.right,
6485 ast::BinaryPart::CallExpressionKw(existing_call)
6486 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6487 );
6488
6489 match (left_is_distance, right_is_distance) {
6490 (true, _) => {
6491 binary_expr.left = call.clone();
6492 binary_expr.right = value.clone();
6493 }
6494 (false, true) => {
6495 binary_expr.left = value.clone();
6496 binary_expr.right = call.clone();
6497 }
6498 (false, false) => return TraversalReturn::new_continue(()),
6499 }
6500
6501 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6502 }
6503 }
6504 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6505 if let NodeMut::BinaryExpression(binary_expr) = node {
6506 let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6507 call
6508 } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6509 call
6510 } else {
6511 return TraversalReturn::new_continue(());
6512 };
6513
6514 if let Some(label_arg) = call
6515 .arguments
6516 .iter_mut()
6517 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6518 {
6519 label_arg.arg = label_position.clone();
6520 } else {
6521 call.arguments.push(ast::LabeledArg {
6522 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6523 arg: label_position.clone(),
6524 });
6525 }
6526
6527 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6528 }
6529 }
6530 AstMutateCommand::EditCallUnlabeled { arg } => {
6531 if let NodeMut::CallExpressionKw(call) = node {
6532 call.unlabeled = Some(arg.clone());
6533 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6534 }
6535 }
6536 AstMutateCommand::EditVarInitialValue { value } => {
6537 if let NodeMut::SketchVar(sketch_var) = node {
6541 let Ok(literal) = to_source_number(*value) else {
6542 return TraversalReturn::new_break(Err(KclError::refactor(format!(
6543 "Could not convert number to AST literal: {:?}",
6544 *value
6545 ))));
6546 };
6547 sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6548 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6549 }
6550 }
6551 AstMutateCommand::DeleteNode => {
6552 return TraversalReturn {
6553 mutate_body_item: MutateBodyItem::Delete,
6554 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6555 };
6556 }
6557 }
6558 TraversalReturn::new_continue(())
6559}
6560
6561struct FindSketchBlockSourceRange {
6562 target_before_mutation: SourceRange,
6564 found: Cell<Option<AstNodeRef>>,
6568}
6569
6570impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6571 type Error = crate::front::Error;
6572
6573 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6574 let Ok(node_range) = SourceRange::try_from(&node) else {
6575 return Ok(true);
6576 };
6577
6578 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6579 if node_range.module_id() == self.target_before_mutation.module_id()
6580 && node_range.start() == self.target_before_mutation.start()
6581 && node_range.end() >= self.target_before_mutation.end()
6583 {
6584 self.found.set(sketch_block.body.items.last().map(|item| match item {
6585 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6589 _ => AstNodeRef::from(item),
6590 }));
6591 return Ok(false);
6592 } else {
6593 return Ok(true);
6596 }
6597 }
6598
6599 for child in node.children().iter() {
6600 if !child.visit(*self)? {
6601 return Ok(false);
6602 }
6603 }
6604
6605 Ok(true)
6606 }
6607}
6608
6609struct FindSketchBlockByNodePath {
6610 target_node_path: ast::NodePath,
6612 found: Cell<Option<AstNodeRef>>,
6616}
6617
6618impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6619 type Error = crate::front::Error;
6620
6621 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6622 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6623 return Ok(true);
6624 };
6625
6626 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6627 if let Some(node_path) = node_path
6628 && node_path == self.target_node_path
6629 {
6630 self.found.set(sketch_block.body.items.last().map(|item| match item {
6631 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6635 _ => AstNodeRef::from(item),
6636 }));
6637
6638 return Ok(false);
6639 } else {
6640 return Ok(true);
6643 }
6644 }
6645
6646 for child in node.children().iter() {
6647 if !child.visit(*self)? {
6648 return Ok(false);
6649 }
6650 }
6651
6652 Ok(true)
6653 }
6654}
6655
6656fn find_sketch_block_added_item(
6664 ast: &ast::Node<ast::Program>,
6665 sketch_block_before_mutation: &AstNodeRef,
6666) -> Result<AstNodeRef, KclError> {
6667 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6668 let find = FindSketchBlockByNodePath {
6669 target_node_path: node_path.clone(),
6670 found: Cell::new(None),
6671 };
6672 let node = crate::walk::Node::from(ast);
6673 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6674 find.found.into_inner().ok_or_else(|| {
6675 KclError::refactor(format!(
6676 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6677 ))
6678 })
6679 } else {
6680 let find = FindSketchBlockSourceRange {
6682 target_before_mutation: sketch_block_before_mutation.range,
6683 found: Cell::new(None),
6684 };
6685 let node = crate::walk::Node::from(ast);
6686 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6687 find.found.into_inner().ok_or_else(|| KclError::refactor(
6688 format!("Source range after mutation not found for range before mutation: {sketch_block_before_mutation:?}; Did you try formatting (i.e. call recast) before calling this?"),
6689 ))
6690 }
6691}
6692
6693fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6694 let message = error.message().trim();
6695 let message = if message.is_empty() {
6696 "unknown parse error"
6697 } else {
6698 message
6699 };
6700
6701 format!("{prefix}: {message}")
6702}
6703
6704fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6705 let (program, errors) = Program::parse(source).map_err(|err| {
6706 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6707 })?;
6708 if !errors.is_empty() {
6709 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6710 format_compilation_issues(parse_error_prefix, &errors),
6711 )));
6712 }
6713
6714 program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6715}
6716
6717fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6718 let Some(first_issue) = issues
6719 .iter()
6720 .find(|issue| issue.severity.is_err())
6721 .or_else(|| issues.first())
6722 else {
6723 return prefix.to_owned();
6724 };
6725
6726 let message = first_issue.message.trim();
6727 let message = if message.is_empty() {
6728 "unknown parse error"
6729 } else {
6730 message
6731 };
6732
6733 if issues.len() > 1 {
6734 format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6735 } else {
6736 format!("{prefix}: {message}")
6737 }
6738}
6739
6740fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6741 ast.recast_top(&Default::default(), 0)
6743}
6744
6745struct FindNumericLiteral {
6746 target: SourceRange,
6747 found: Cell<Option<ast::NumericLiteral>>,
6748}
6749
6750impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6751 type Error = crate::front::Error;
6752
6753 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6754 let Ok(node_range) = SourceRange::try_from(&node) else {
6755 return Ok(true);
6756 };
6757
6758 if node_range == self.target
6759 && let crate::walk::Node::NumericLiteral(literal) = node
6760 {
6761 self.found.set(Some(literal.inner.clone()));
6762 return Ok(false);
6763 }
6764
6765 for child in node.children().iter() {
6766 if !child.visit(*self)? {
6767 return Ok(false);
6768 }
6769 }
6770
6771 Ok(true)
6772 }
6773}
6774
6775fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6776 let find = FindNumericLiteral {
6777 target,
6778 found: Cell::new(None),
6779 };
6780 let node = crate::walk::Node::from(ast);
6781 node.visit(&find).ok()?;
6782 find.found.into_inner()
6783}
6784
6785struct FindSketchVarInitialByNodePath<'a> {
6786 target: &'a ast::NodePath,
6787 sketch_var_found: Cell<bool>,
6788 initial_literal: Cell<Option<ast::NumericLiteral>>,
6789}
6790
6791impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6792 type Error = crate::front::Error;
6793
6794 fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6795 if let crate::walk::Node::SketchVar(sketch_var) = node
6796 && sketch_var.node_path.as_ref() == Some(self.target)
6797 {
6798 self.sketch_var_found.set(true);
6799 if let Some(initial) = &sketch_var.initial {
6800 self.initial_literal.set(Some(initial.inner.clone()));
6801 }
6802 return Ok(false);
6803 }
6804
6805 for child in node.children().iter() {
6806 if !child.visit(*self)? {
6807 return Ok(false);
6808 }
6809 }
6810
6811 Ok(true)
6812 }
6813}
6814
6815fn numeric_literal_at_node_path(
6825 ast: &ast::Node<ast::Program>,
6826 node_path: Option<&ast::NodePath>,
6827 source_range: SourceRange,
6828) -> Option<Option<ast::NumericLiteral>> {
6829 let Some(node_path) = node_path else {
6830 let message = "numeric_literal_at_node_path: missing node_path on var solution; falling back to source-range lookup, which can fail under whitespace shifts";
6831 #[cfg(target_arch = "wasm32")]
6832 web_sys::console::warn_1(&message.into());
6833 #[cfg(not(target_arch = "wasm32"))]
6834 eprintln!("WARNING: {message}");
6835 return numeric_literal_at_source_range(ast, source_range).map(Some);
6836 };
6837 let find = FindSketchVarInitialByNodePath {
6838 target: node_path,
6839 sketch_var_found: Cell::new(false),
6840 initial_literal: Cell::new(None),
6841 };
6842 let node = crate::walk::Node::from(ast);
6843 node.visit(&find).ok()?;
6844 if !find.sketch_var_found.get() {
6845 return None;
6846 }
6847 Some(find.initial_literal.into_inner())
6848}
6849
6850fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6851 match suffix {
6852 NumericSuffix::Mm => Some(UnitLength::Millimeters),
6853 NumericSuffix::Cm => Some(UnitLength::Centimeters),
6854 NumericSuffix::M => Some(UnitLength::Meters),
6855 NumericSuffix::Inch => Some(UnitLength::Inches),
6856 NumericSuffix::Ft => Some(UnitLength::Feet),
6857 NumericSuffix::Yd => Some(UnitLength::Yards),
6858 _ => None,
6859 }
6860}
6861
6862fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6863 match suffix_length_unit(number.units) {
6864 Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6865 None => number.value,
6866 }
6867}
6868
6869fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6870 match suffix_length_unit(literal.suffix) {
6871 Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6872 None => literal.value,
6873 }
6874}
6875
6876fn var_solution_needs_commit(
6877 current_literal: &ast::NumericLiteral,
6878 solved_value: Number,
6879 default_length_unit: UnitLength,
6880) -> bool {
6881 let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6882 let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6883
6884 (current - solved).abs() > 1e-9
6885}
6886
6887fn preserve_var_solution_literal_style(
6888 current_literal: &ast::NumericLiteral,
6889 solved_value: Number,
6890 default_length_unit: UnitLength,
6891) -> Number {
6892 if current_literal.suffix == NumericSuffix::None {
6893 return Number {
6894 value: number_value_in_default_length_units(solved_value, default_length_unit),
6895 units: NumericSuffix::None,
6896 };
6897 }
6898
6899 let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6900 return solved_value;
6901 };
6902
6903 let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6904 Number {
6905 value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6906 units: current_literal.suffix,
6907 }
6908}
6909
6910pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6911 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6912 inner: ast::ArrayExpression {
6913 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6914 non_code_meta: Default::default(),
6915 digest: None,
6916 },
6917 start: Default::default(),
6918 end: Default::default(),
6919 module_id: Default::default(),
6920 node_path: None,
6921 outer_attrs: Default::default(),
6922 pre_comments: Default::default(),
6923 comment_start: Default::default(),
6924 })))
6925}
6926
6927pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6928 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6929 ast::ArrayExpression {
6930 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6931 digest: None,
6932 non_code_meta: Default::default(),
6933 },
6934 ))))
6935}
6936
6937fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6938 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6939 ast::ArrayExpression {
6940 elements: vec![
6941 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6942 point.x,
6943 )?)))),
6944 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6945 point.y,
6946 )?)))),
6947 ],
6948 non_code_meta: Default::default(),
6949 digest: None,
6950 },
6951 ))))
6952}
6953
6954fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6955 match expr {
6956 Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6957 inner: ast::Literal::from(to_source_number(*number)?),
6958 start: Default::default(),
6959 end: Default::default(),
6960 module_id: Default::default(),
6961 node_path: None,
6962 outer_attrs: Default::default(),
6963 pre_comments: Default::default(),
6964 comment_start: Default::default(),
6965 }))),
6966 Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6967 inner: ast::SketchVar {
6968 initial: Some(BoxNode::new(ast::Node {
6969 inner: to_source_number(*number)?,
6970 start: Default::default(),
6971 end: Default::default(),
6972 module_id: Default::default(),
6973 node_path: None,
6974 outer_attrs: Default::default(),
6975 pre_comments: Default::default(),
6976 comment_start: Default::default(),
6977 })),
6978 digest: None,
6979 },
6980 start: Default::default(),
6981 end: Default::default(),
6982 module_id: Default::default(),
6983 node_path: None,
6984 outer_attrs: Default::default(),
6985 pre_comments: Default::default(),
6986 comment_start: Default::default(),
6987 }))),
6988 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6989 }
6990}
6991
6992fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6993 Ok(ast::NumericLiteral {
6994 value: number.value,
6995 suffix: number.units,
6996 raw: format_number_literal(number.value, number.units, None)?,
6997 digest: None,
6998 })
6999}
7000
7001pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7002 ast::Expr::Name(BoxNode::new(ast_name(name)))
7003}
7004
7005fn ast_name(name: String) -> ast::Node<ast::Name> {
7006 ast::Node {
7007 inner: ast::Name {
7008 name: ast::Node {
7009 inner: ast::Identifier { name, digest: None },
7010 start: Default::default(),
7011 end: Default::default(),
7012 module_id: Default::default(),
7013 node_path: None,
7014 outer_attrs: Default::default(),
7015 pre_comments: Default::default(),
7016 comment_start: Default::default(),
7017 },
7018 path: Vec::new(),
7019 abs_path: false,
7020 digest: None,
7021 },
7022 start: Default::default(),
7023 end: Default::default(),
7024 module_id: Default::default(),
7025 node_path: None,
7026 outer_attrs: Default::default(),
7027 pre_comments: Default::default(),
7028 comment_start: Default::default(),
7029 }
7030}
7031
7032pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7033 ast::Name {
7034 name: ast::Node {
7035 inner: ast::Identifier {
7036 name: name.to_owned(),
7037 digest: None,
7038 },
7039 start: Default::default(),
7040 end: Default::default(),
7041 module_id: Default::default(),
7042 node_path: None,
7043 outer_attrs: Default::default(),
7044 pre_comments: Default::default(),
7045 comment_start: Default::default(),
7046 },
7047 path: Default::default(),
7048 abs_path: false,
7049 digest: None,
7050 }
7051}
7052
7053pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
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_line_ast(start_ast: ast::Expr, end_ast: 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(LINE_FN)),
7081 unlabeled: None,
7082 arguments: vec![
7083 ast::LabeledArg {
7084 label: Some(ast::Identifier::new(LINE_START_PARAM)),
7085 arg: start_ast,
7086 },
7087 ast::LabeledArg {
7088 label: Some(ast::Identifier::new(LINE_END_PARAM)),
7089 arg: end_ast,
7090 },
7091 ],
7092 digest: None,
7093 non_code_meta: Default::default(),
7094 })))
7095}
7096
7097pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
7099 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7100 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
7101 unlabeled: None,
7102 arguments: vec![
7103 ast::LabeledArg {
7104 label: Some(ast::Identifier::new(ARC_START_PARAM)),
7105 arg: start_ast,
7106 },
7107 ast::LabeledArg {
7108 label: Some(ast::Identifier::new(ARC_END_PARAM)),
7109 arg: end_ast,
7110 },
7111 ast::LabeledArg {
7112 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
7113 arg: center_ast,
7114 },
7115 ],
7116 digest: None,
7117 non_code_meta: Default::default(),
7118 })))
7119}
7120
7121pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
7123 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7124 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
7125 unlabeled: None,
7126 arguments: vec![
7127 ast::LabeledArg {
7128 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
7129 arg: start_ast,
7130 },
7131 ast::LabeledArg {
7132 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
7133 arg: center_ast,
7134 },
7135 ],
7136 digest: None,
7137 non_code_meta: Default::default(),
7138 })))
7139}
7140
7141pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7143 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7144 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7145 unlabeled: Some(line_expr),
7146 arguments: Default::default(),
7147 digest: None,
7148 non_code_meta: Default::default(),
7149 })))
7150}
7151
7152pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7154 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7155 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7156 unlabeled: Some(line_expr),
7157 arguments: Default::default(),
7158 digest: None,
7159 non_code_meta: Default::default(),
7160 })))
7161}
7162
7163pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7165 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7166 object: object_expr,
7167 property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7168 name: ast::Node::no_src(ast::Identifier {
7169 name: property.to_string(),
7170 digest: None,
7171 }),
7172 path: Vec::new(),
7173 abs_path: false,
7174 digest: None,
7175 }))),
7176 computed: false,
7177 digest: None,
7178 })))
7179}
7180
7181pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7182 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7183 object: object_expr,
7184 property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7185 ast::NumericLiteral {
7186 value: index as f64,
7187 suffix: NumericSuffix::None,
7188 raw: index.to_string(),
7189 digest: None,
7190 },
7191 )))),
7192 computed: true,
7193 digest: None,
7194 })))
7195}
7196
7197fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7199 let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7201 position.x,
7202 )?))));
7203 let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7204 position.y,
7205 )?))));
7206 let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7207 elements: vec![x_literal, y_literal],
7208 digest: None,
7209 non_code_meta: Default::default(),
7210 })));
7211
7212 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7214 elements: vec![point_expr, point_array],
7215 digest: None,
7216 non_code_meta: Default::default(),
7217 })));
7218
7219 Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7221 ast::CallExpressionKw {
7222 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7223 unlabeled: Some(array_expr),
7224 arguments: Default::default(),
7225 digest: None,
7226 non_code_meta: Default::default(),
7227 },
7228 ))))
7229}
7230
7231pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7233 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7234 elements: line_exprs,
7235 digest: None,
7236 non_code_meta: Default::default(),
7237 })));
7238
7239 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7241 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7242 unlabeled: Some(array_expr),
7243 arguments: Default::default(),
7244 digest: None,
7245 non_code_meta: Default::default(),
7246 })))
7247}
7248
7249pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7251 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7252 elements: segment_exprs,
7253 digest: None,
7254 non_code_meta: Default::default(),
7255 })));
7256
7257 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7258 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7259 unlabeled: Some(array_expr),
7260 arguments: Default::default(),
7261 digest: None,
7262 non_code_meta: Default::default(),
7263 })))
7264}
7265
7266pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7268 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7269 elements: vec![seg1_expr, seg2_expr],
7270 digest: None,
7271 non_code_meta: Default::default(),
7272 })));
7273
7274 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7275 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7276 unlabeled: Some(array_expr),
7277 arguments: Default::default(),
7278 digest: None,
7279 non_code_meta: Default::default(),
7280 })))
7281}
7282
7283pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7285 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7286 elements: input_exprs,
7287 digest: None,
7288 non_code_meta: Default::default(),
7289 })));
7290 let arguments = vec![ast::LabeledArg {
7291 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7292 arg: axis_expr,
7293 }];
7294
7295 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7296 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7297 unlabeled: Some(array_expr),
7298 arguments,
7299 digest: None,
7300 non_code_meta: Default::default(),
7301 })))
7302}
7303
7304pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7306 let arguments = vec![ast::LabeledArg {
7307 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7308 arg: point_expr,
7309 }];
7310
7311 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7312 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7313 unlabeled: Some(segment_expr),
7314 arguments,
7315 digest: None,
7316 non_code_meta: Default::default(),
7317 })))
7318}
7319
7320fn issue_source_range(error: &KclError) -> SourceRange {
7325 let source_ranges = error.source_ranges();
7326 source_ranges
7327 .iter()
7328 .find(|range| range.is_top_level_module())
7329 .or_else(|| source_ranges.first())
7330 .copied()
7331 .unwrap_or_else(SourceRange::synthetic)
7332}
7333
7334#[cfg(test)]
7335mod tests {
7336 use std::sync;
7337
7338 use super::*;
7339 use crate::engine::PlaneName;
7340 use crate::engine::engine_manager::EngineManager;
7341 use crate::execution::cache::SketchModeState;
7342 use crate::execution::cache::clear_mem_cache;
7343 use crate::execution::cache::read_old_memory;
7344 use crate::execution::cache::write_old_memory;
7345 use crate::front::Distance;
7346 use crate::front::Fixed;
7347 use crate::front::FixedPoint;
7348 use crate::front::Midpoint;
7349 use crate::front::Object;
7350 use crate::front::Plane;
7351 use crate::front::Sketch;
7352 use crate::front::Tangent;
7353 use crate::frontend::sketch::Vertical;
7354 use crate::pretty::NumericSuffix;
7355
7356 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7357 for object in &scene_graph.objects {
7358 if let ObjectKind::Sketch(_) = &object.kind {
7359 return Some(object);
7360 }
7361 }
7362 None
7363 }
7364
7365 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7366 for object in &scene_graph.objects {
7367 if let ObjectKind::Face(_) = &object.kind {
7368 return Some(object);
7369 }
7370 }
7371 None
7372 }
7373
7374 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7375 for object in &scene_graph.objects {
7376 if matches!(&object.kind, ObjectKind::Wall(_)) {
7377 return Some(object.id);
7378 }
7379 }
7380 None
7381 }
7382
7383 fn find_cap_object_id_with_solid_output_index(
7384 scene_graph: &SceneGraph,
7385 cap_kind: crate::frontend::api::CapKind,
7386 solid_output_index: usize,
7387 ) -> Option<ObjectId> {
7388 for object in &scene_graph.objects {
7389 if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7390 {
7391 return Some(object.id);
7392 }
7393 }
7394 None
7395 }
7396
7397 #[test]
7398 fn issue_source_range_prefers_top_level_module() {
7399 use kcl_error::ModuleId;
7400
7401 let top = SourceRange::new(10, 20, ModuleId::default());
7402 let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7403
7404 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7407 "boom".to_owned(),
7408 vec![imported, top],
7409 ));
7410 assert_eq!(super::issue_source_range(&error), top);
7411
7412 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7414 assert_eq!(super::issue_source_range(&error), imported);
7415
7416 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7418 assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7419 }
7420
7421 #[test]
7422 fn composite_constituent_sweeps_are_not_solid_outputs() {
7423 use kcl_api::artifact::ArtifactSweepMethod;
7424 use kcl_api::artifact::CompositeSolid;
7425 use kcl_api::artifact::CompositeSolidSubType;
7426 use kcl_api::artifact::Sweep;
7427 use kcl_api::artifact::SweepSubType;
7428
7429 let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7430 let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7431 let composite_id = ArtifactId::new(Uuid::new_v4());
7432 let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7433 let sweep = |id| {
7434 Artifact::Sweep(Sweep {
7435 id,
7436 sub_type: SweepSubType::Extrusion,
7437 path_id: ArtifactId::new(Uuid::new_v4()),
7438 surface_ids: Vec::new(),
7439 edge_ids: Vec::new(),
7440 code_ref: code_ref.clone(),
7441 source_sweep_id: None,
7442 trajectory_id: None,
7443 method: ArtifactSweepMethod::New,
7444 consumed: false,
7445 pattern_ids: Vec::new(),
7446 })
7447 };
7448 let mut artifacts = IndexMap::from([
7449 (first_sweep_id, sweep(first_sweep_id)),
7450 (second_sweep_id, sweep(second_sweep_id)),
7451 ]);
7452
7453 let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7454 assert_eq!(
7455 solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7456 Some(0)
7457 );
7458 assert_eq!(
7459 solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7460 Some(1)
7461 );
7462
7463 artifacts.insert(
7464 composite_id,
7465 Artifact::CompositeSolid(CompositeSolid {
7466 id: composite_id,
7467 consumed: false,
7468 sub_type: CompositeSolidSubType::Union,
7469 output_index: None,
7470 solid_ids: vec![first_sweep_id, second_sweep_id],
7471 tool_ids: Vec::new(),
7472 code_ref,
7473 composite_solid_id: None,
7474 pattern_ids: Vec::new(),
7475 }),
7476 );
7477 let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7478 assert_eq!(
7479 solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7480 None
7481 );
7482 assert_eq!(
7483 solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7484 None
7485 );
7486 }
7487
7488 #[test]
7489 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7490 let source = "\
7491region001 = region(point = [0.1, 0.1], sketch = s)
7492extrude001 = extrude(region001, length = 5)
7493revolve001 = revolve(region001, axis = Y)
7494sweep001 = sweep(region001, path = path001)
7495loft001 = loft(region001)
7496not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7497";
7498
7499 let program = Program::parse(source).unwrap().0.unwrap();
7500
7501 assert_eq!(
7502 region_name_from_sweep_variable(&program.ast, "extrude001"),
7503 Some("region001".to_owned())
7504 );
7505 assert_eq!(
7506 region_name_from_sweep_variable(&program.ast, "revolve001"),
7507 Some("region001".to_owned())
7508 );
7509 assert_eq!(
7510 region_name_from_sweep_variable(&program.ast, "sweep001"),
7511 Some("region001".to_owned())
7512 );
7513 assert_eq!(
7514 region_name_from_sweep_variable(&program.ast, "loft001"),
7515 Some("region001".to_owned())
7516 );
7517 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7518 }
7519
7520 #[track_caller]
7521 fn expect_sketch(object: &Object) -> &Sketch {
7522 if let ObjectKind::Sketch(sketch) = &object.kind {
7523 sketch
7524 } else {
7525 panic!("Object is not a sketch: {:?}", object);
7526 }
7527 }
7528
7529 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7530 let point_object = scene_graph.objects.get(point_id.0).unwrap();
7531 let ObjectKind::Segment {
7532 segment: Segment::Point(point),
7533 } = &point_object.kind
7534 else {
7535 panic!("Object is not a point segment: {point_object:?}");
7536 };
7537 point.position.clone()
7538 }
7539
7540 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7541 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7542 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7543 }
7544
7545 fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7548 Point2d {
7549 x: Expr::Var(Number {
7550 value: x,
7551 units: NumericSuffix::Mm,
7552 }),
7553 y: Expr::Var(Number {
7554 value: y,
7555 units: NumericSuffix::Mm,
7556 }),
7557 }
7558 }
7559
7560 fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7563 Point2d {
7564 x: Number {
7565 value: x,
7566 units: NumericSuffix::Mm,
7567 },
7568 y: Number {
7569 value: y,
7570 units: NumericSuffix::Mm,
7571 },
7572 }
7573 }
7574
7575 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7576 LineCtor {
7577 start: Point2d {
7578 x: Expr::Number(Number { value: start_x, units }),
7579 y: Expr::Number(Number { value: start_y, units }),
7580 },
7581 end: Point2d {
7582 x: Expr::Number(Number { value: end_x, units }),
7583 y: Expr::Number(Number { value: end_y, units }),
7584 },
7585 construction: None,
7586 }
7587 }
7588
7589 async fn create_sketch_with_single_line(
7590 frontend: &mut FrontendState,
7591 ctx: &ExecutorContext,
7592 mock_ctx: &ExecutorContext,
7593 version: Version,
7594 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7595 frontend.program = Program::empty();
7596
7597 let sketch_args = SketchCtor {
7598 on: Plane::Default(PlaneName::Xy),
7599 };
7600 let (_src_delta, _scene_delta, sketch_id) = frontend
7601 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7602 .await
7603 .unwrap();
7604
7605 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7606 let (source_delta, scene_graph_delta) = frontend
7607 .add_segment(mock_ctx, version, sketch_id, segment, None)
7608 .await
7609 .unwrap();
7610 let line_id = *scene_graph_delta
7611 .new_objects
7612 .last()
7613 .expect("Expected line object id to be created");
7614
7615 (sketch_id, line_id, source_delta, scene_graph_delta)
7616 }
7617
7618 async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7619 frontend.program = program.clone();
7620 let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7621 frontend.update_state_after_exec(outcome, true);
7622 }
7623
7624 #[test]
7625 fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7626 for (source, expected_message) in [
7627 ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7628 ("3'", "Error parsing KCL source after editing: found unknown token '''"),
7629 ] {
7630 let err = parse_frontend_mutation_source(
7631 source,
7632 "Error parsing KCL source after editing",
7633 "No AST produced after editing",
7634 )
7635 .expect_err("expected invalid KCL source to fail");
7636 let message = err.error.message();
7637
7638 assert_eq!(message, expected_message);
7639 assert!(!message.contains("CompilationIssue"));
7640 assert!(!message.contains("KclErrorDetails"));
7641 assert!(!message.contains("source_range"));
7642 }
7643 }
7644
7645 #[tokio::test(flavor = "multi_thread")]
7646 async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7647 let initial_source = "\
7648sketch(on = XY) {
7649 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7650 distance([line1.start, line1.end]) == 10
7651}
7652";
7653 let program = Program::parse(initial_source).unwrap().0.unwrap();
7654
7655 let mut frontend = FrontendState::new();
7656 let mock_ctx = ExecutorContext::new_mock(None).await;
7657 let version = Version(0);
7658
7659 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7660 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7661 let sketch_id = sketch_object.id;
7662 let sketch = expect_sketch(sketch_object);
7663 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7664
7665 for (value, expected_message) in [
7666 ("**", "Invalid constraint value: Unexpected token: *"),
7667 ("3'", "Invalid constraint value: found unknown token '''"),
7668 ] {
7669 let err = frontend
7670 .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7671 .await
7672 .expect_err("expected invalid constraint expression to fail");
7673 let message = err.error.message();
7674
7675 assert_eq!(message, expected_message);
7676 assert!(!message.contains("CompilationIssue"));
7677 assert!(!message.contains("KclErrorDetails"));
7678 assert!(!message.contains("source_range"));
7679 }
7680
7681 mock_ctx.close().await;
7682 }
7683
7684 #[tokio::test(flavor = "multi_thread")]
7685 async fn test_failed_edit_constraint_value_does_not_update_program() {
7686 let initial_source = "\
7687sketch(on = XY) {
7688 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7689 distance([line1.start, line1.end]) == 10
7690}
7691";
7692 let program = Program::parse(initial_source).unwrap().0.unwrap();
7693 let original_source = program.original_file_contents.clone();
7694
7695 let mut frontend = FrontendState::new();
7696 let mock_ctx = ExecutorContext::new_mock(None).await;
7697 let version = Version(0);
7698
7699 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7700 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7701 let sketch_id = sketch_object.id;
7702 let sketch = expect_sketch(sketch_object);
7703 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7704
7705 frontend
7706 .edit_constraint_value(
7707 &mock_ctx,
7708 version,
7709 sketch_id,
7710 constraint_id,
7711 "unknownDistance".to_owned(),
7712 )
7713 .await
7714 .expect_err("expected invalid constraint value to fail execution");
7715
7716 assert_eq!(frontend.program.original_file_contents, original_source);
7717 assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7718
7719 mock_ctx.close().await;
7720 }
7721
7722 #[tokio::test(flavor = "multi_thread")]
7723 async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7724 let initial_source = "\
7725arr = [0]
7726sketch(on = XY) {
7727 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7728 distance([line1.start, line1.end]) == 10
7729}
7730";
7731 let program = Program::parse(initial_source).unwrap().0.unwrap();
7732
7733 let mut frontend = FrontendState::new();
7734 let mock_ctx = ExecutorContext::new_mock(None).await;
7735 let version = Version(0);
7736
7737 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7738 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7739 let sketch_id = sketch_object.id;
7740 let sketch = expect_sketch(sketch_object);
7741 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7742
7743 let err = frontend
7747 .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7748 .await
7749 .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7750 let message = err.error.message();
7751 assert!(
7752 message.contains("The array doesn't have any item at index 5"),
7753 "unexpected error message: {message}"
7754 );
7755
7756 mock_ctx.close().await;
7757 }
7758
7759 #[tokio::test(flavor = "multi_thread")]
7760 async fn test_sketch_checkpoint_round_trip_restores_state() {
7761 let mut frontend = FrontendState::new();
7762 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7763 let mock_ctx = ExecutorContext::new_mock(None).await;
7764 let version = Version(0);
7765
7766 let (sketch_id, line_id, source_delta, scene_graph_delta) =
7767 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7768
7769 let expected_source = source_delta.text.clone();
7770 let expected_scene_graph = frontend.scene_graph.clone();
7771 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7772 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7773
7774 let checkpoint_id = frontend
7775 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7776 .await
7777 .unwrap();
7778
7779 let edited_segments = vec![ExistingSegmentCtor {
7780 id: line_id,
7781 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7782 }];
7783 let (edited_source, _edited_scene) = frontend
7784 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7785 .await
7786 .unwrap();
7787 assert_ne!(edited_source.text, expected_source);
7788
7789 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7790
7791 assert_eq!(restored.source_delta.text, expected_source);
7792 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7793 assert!(restored.scene_graph_delta.invalidates_ids);
7794 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7795 assert_eq!(frontend.scene_graph, expected_scene_graph);
7796 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7797
7798 ctx.close().await;
7799 }
7800
7801 #[tokio::test(flavor = "multi_thread")]
7802 async fn test_sketch_checkpoints_prune_oldest_entries() {
7803 let mut frontend = FrontendState::new();
7804 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7805 let mock_ctx = ExecutorContext::new_mock(None).await;
7806 let version = Version(0);
7807
7808 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7809 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7810
7811 let mut checkpoint_ids = Vec::new();
7812 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7813 checkpoint_ids.push(
7814 frontend
7815 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7816 .await
7817 .unwrap(),
7818 );
7819 }
7820
7821 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7822 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7823
7824 let oldest_retained = checkpoint_ids[3];
7825 assert_eq!(
7826 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7827 Some(oldest_retained)
7828 );
7829
7830 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7831 assert!(evicted_restore.is_err());
7832 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7833
7834 frontend
7835 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7836 .await
7837 .unwrap();
7838
7839 ctx.close().await;
7840 }
7841
7842 #[tokio::test(flavor = "multi_thread")]
7843 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7844 let mut frontend = FrontendState::new();
7845 let missing_checkpoint = SketchCheckpointId::new(999);
7846
7847 let err = frontend
7848 .restore_sketch_checkpoint(missing_checkpoint)
7849 .await
7850 .expect_err("Expected restore to fail for missing checkpoint");
7851
7852 assert!(err.msg.contains("Sketch checkpoint not found"));
7853 }
7854
7855 #[tokio::test(flavor = "multi_thread")]
7856 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7857 let mut frontend = FrontendState::new();
7858 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7859 let mock_ctx = ExecutorContext::new_mock(None).await;
7860 let version = Version(0);
7861
7862 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7863 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7864
7865 let checkpoint_a = frontend
7866 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7867 .await
7868 .unwrap();
7869 let checkpoint_b = frontend
7870 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7871 .await
7872 .unwrap();
7873 assert_eq!(frontend.sketch_checkpoints.len(), 2);
7874
7875 frontend.clear_sketch_checkpoints();
7876 assert!(frontend.sketch_checkpoints.is_empty());
7877 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7878 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7879
7880 ctx.close().await;
7881 }
7882
7883 #[tokio::test(flavor = "multi_thread")]
7884 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7885 let mut frontend = FrontendState::new();
7886 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7887 let mock_ctx = ExecutorContext::new_mock(None).await;
7888 let version = Version(0);
7889
7890 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7891 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7892 let old_source = source_delta.text.clone();
7893 let old_checkpoint = frontend
7894 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7895 .await
7896 .unwrap();
7897 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7898
7899 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
7900 .unwrap()
7901 .0
7902 .unwrap();
7903
7904 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7905 let SetProgramOutcome::Success {
7906 checkpoint_id: Some(new_checkpoint),
7907 ..
7908 } = result
7909 else {
7910 panic!("Expected Success with a fresh checkpoint baseline");
7911 };
7912
7913 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7914
7915 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7916 assert_eq!(old_restore.source_delta.text, old_source);
7917
7918 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7919 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7920
7921 ctx.close().await;
7922 }
7923
7924 #[tokio::test(flavor = "multi_thread")]
7925 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7926 let mut frontend = FrontendState::new();
7927 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7928 let mock_ctx = ExecutorContext::new_mock(None).await;
7929 let version = Version(0);
7930
7931 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7932 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7933 let old_checkpoint = frontend
7934 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7935 .await
7936 .unwrap();
7937 let checkpoint_count_before = frontend.sketch_checkpoints.len();
7938
7939 let failing_program = Program::parse(
7940 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7941 )
7942 .unwrap()
7943 .0
7944 .unwrap();
7945
7946 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7947 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7948 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7949 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7950
7951 ctx.close().await;
7952 }
7953
7954 #[tokio::test(flavor = "multi_thread")]
7955 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7956 let mut frontend = FrontendState::new();
7957 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7958
7959 let program = Program::parse(
7960 "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",
7961 )
7962 .unwrap()
7963 .0
7964 .unwrap();
7965 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7966 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7967 panic!("Expected successful baseline program execution");
7968 };
7969
7970 clear_mem_cache().await;
7971 assert!(read_old_memory().await.is_none());
7972
7973 let checkpoint_without_mock_memory = frontend
7974 .create_sketch_checkpoint((*exec_outcome).clone())
7975 .await
7976 .unwrap();
7977
7978 write_old_memory(SketchModeState::new_for_tests()).await;
7979 assert!(read_old_memory().await.is_some());
7980
7981 let checkpoint_with_mock_memory = frontend
7982 .create_sketch_checkpoint((*exec_outcome).clone())
7983 .await
7984 .unwrap();
7985
7986 clear_mem_cache().await;
7987 assert!(read_old_memory().await.is_none());
7988
7989 frontend
7990 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7991 .await
7992 .unwrap();
7993 assert!(read_old_memory().await.is_some());
7994
7995 frontend
7996 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7997 .await
7998 .unwrap();
7999 assert!(read_old_memory().await.is_none());
8000
8001 ctx.close().await;
8002 }
8003
8004 #[tokio::test(flavor = "multi_thread")]
8005 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
8006 let source = "\
8007sketch(on = XY) {
8008 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
8009}
8010
8011bad = missing_name
8012";
8013 let program = Program::parse(source).unwrap().0.unwrap();
8014
8015 let mut frontend = FrontendState::new();
8016
8017 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8018 let mock_ctx = ExecutorContext::new_mock(None).await;
8019 let version = Version(0);
8020 let project_id = ProjectId(0);
8021 let file_id = FileId(0);
8022
8023 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
8024 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
8025 };
8026
8027 let sketch_id = frontend
8028 .scene_graph
8029 .objects
8030 .iter()
8031 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
8032 .expect("Expected sketch object from errored hack_set_program");
8033
8034 frontend
8035 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
8036 .await
8037 .unwrap();
8038
8039 ctx.close().await;
8040 mock_ctx.close().await;
8041 }
8042
8043 #[tokio::test(flavor = "multi_thread")]
8044 async fn test_new_sketch_add_point_edit_point() {
8045 let program = Program::empty();
8046
8047 let mut frontend = FrontendState::new();
8048 frontend.program = program;
8049
8050 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8051 let mock_ctx = ExecutorContext::new_mock(None).await;
8052 let version = Version(0);
8053
8054 let sketch_args = SketchCtor {
8055 on: Plane::Default(PlaneName::Xy),
8056 };
8057 let (_src_delta, scene_delta, sketch_id) = frontend
8058 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8059 .await
8060 .unwrap();
8061 assert_eq!(sketch_id, ObjectId(1));
8062 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8063 let sketch_object = &scene_delta.new_graph.objects[1];
8064 assert_eq!(sketch_object.id, ObjectId(1));
8065 assert_eq!(
8066 sketch_object.kind,
8067 ObjectKind::Sketch(Sketch {
8068 args: SketchCtor {
8069 on: Plane::Default(PlaneName::Xy)
8070 },
8071 plane: ObjectId(0),
8072 segments: vec![],
8073 constraints: vec![],
8074 })
8075 );
8076 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8077
8078 let point_ctor = PointCtor {
8079 position: Point2d {
8080 x: Expr::Number(Number {
8081 value: 1.0,
8082 units: NumericSuffix::Inch,
8083 }),
8084 y: Expr::Number(Number {
8085 value: 2.0,
8086 units: NumericSuffix::Inch,
8087 }),
8088 },
8089 };
8090 let segment = SegmentCtor::Point(point_ctor);
8091 let (src_delta, scene_delta) = frontend
8092 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8093 .await
8094 .unwrap();
8095 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8096 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8097 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8098 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8099 assert_eq!(scene_object.id.0, i);
8100 }
8101
8102 let point_id = *scene_delta.new_objects.last().unwrap();
8103
8104 let point_ctor = PointCtor {
8105 position: Point2d {
8106 x: Expr::Number(Number {
8107 value: 3.0,
8108 units: NumericSuffix::Inch,
8109 }),
8110 y: Expr::Number(Number {
8111 value: 4.0,
8112 units: NumericSuffix::Inch,
8113 }),
8114 },
8115 };
8116 let segments = vec![ExistingSegmentCtor {
8117 id: point_id,
8118 ctor: SegmentCtor::Point(point_ctor),
8119 }];
8120 let (src_delta, scene_delta) = frontend
8121 .edit_segments(&mock_ctx, version, sketch_id, segments)
8122 .await
8123 .unwrap();
8124 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8125 assert_eq!(scene_delta.new_objects, vec![]);
8126 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8127
8128 ctx.close().await;
8129 mock_ctx.close().await;
8130 }
8131
8132 #[tokio::test(flavor = "multi_thread")]
8133 async fn test_new_sketch_add_line_edit_line() {
8134 let program = Program::empty();
8135
8136 let mut frontend = FrontendState::new();
8137 frontend.program = program;
8138
8139 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8140 let mock_ctx = ExecutorContext::new_mock(None).await;
8141 let version = Version(0);
8142
8143 let sketch_args = SketchCtor {
8144 on: Plane::Default(PlaneName::Xy),
8145 };
8146 let (_src_delta, scene_delta, sketch_id) = frontend
8147 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8148 .await
8149 .unwrap();
8150 assert_eq!(sketch_id, ObjectId(1));
8151 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8152 let sketch_object = &scene_delta.new_graph.objects[1];
8153 assert_eq!(sketch_object.id, ObjectId(1));
8154 assert_eq!(
8155 sketch_object.kind,
8156 ObjectKind::Sketch(Sketch {
8157 args: SketchCtor {
8158 on: Plane::Default(PlaneName::Xy)
8159 },
8160 plane: ObjectId(0),
8161 segments: vec![],
8162 constraints: vec![],
8163 })
8164 );
8165 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8166
8167 let line_ctor = LineCtor {
8168 start: Point2d {
8169 x: Expr::Number(Number {
8170 value: 0.0,
8171 units: NumericSuffix::Mm,
8172 }),
8173 y: Expr::Number(Number {
8174 value: 0.0,
8175 units: NumericSuffix::Mm,
8176 }),
8177 },
8178 end: Point2d {
8179 x: Expr::Number(Number {
8180 value: 10.0,
8181 units: NumericSuffix::Mm,
8182 }),
8183 y: Expr::Number(Number {
8184 value: 10.0,
8185 units: NumericSuffix::Mm,
8186 }),
8187 },
8188 construction: None,
8189 };
8190 let segment = SegmentCtor::Line(line_ctor);
8191 let (src_delta, scene_delta) = frontend
8192 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8193 .await
8194 .unwrap();
8195 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8196 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8197 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8198 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8199 assert_eq!(scene_object.id.0, i);
8200 }
8201
8202 let line = *scene_delta.new_objects.last().unwrap();
8204
8205 let line_ctor = LineCtor {
8206 start: Point2d {
8207 x: Expr::Number(Number {
8208 value: 1.0,
8209 units: NumericSuffix::Mm,
8210 }),
8211 y: Expr::Number(Number {
8212 value: 2.0,
8213 units: NumericSuffix::Mm,
8214 }),
8215 },
8216 end: Point2d {
8217 x: Expr::Number(Number {
8218 value: 13.0,
8219 units: NumericSuffix::Mm,
8220 }),
8221 y: Expr::Number(Number {
8222 value: 14.0,
8223 units: NumericSuffix::Mm,
8224 }),
8225 },
8226 construction: None,
8227 };
8228 let segments = vec![ExistingSegmentCtor {
8229 id: line,
8230 ctor: SegmentCtor::Line(line_ctor),
8231 }];
8232 let (src_delta, scene_delta) = frontend
8233 .edit_segments(&mock_ctx, version, sketch_id, segments)
8234 .await
8235 .unwrap();
8236 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8237 assert_eq!(scene_delta.new_objects, vec![]);
8238 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8239
8240 ctx.close().await;
8241 mock_ctx.close().await;
8242 }
8243
8244 #[tokio::test(flavor = "multi_thread")]
8245 async fn test_new_sketch_add_arc_edit_arc() {
8246 let program = Program::empty();
8247
8248 let mut frontend = FrontendState::new();
8249 frontend.program = program;
8250
8251 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8252 let mock_ctx = ExecutorContext::new_mock(None).await;
8253 let version = Version(0);
8254
8255 let sketch_args = SketchCtor {
8256 on: Plane::Default(PlaneName::Xy),
8257 };
8258 let (_src_delta, scene_delta, sketch_id) = frontend
8259 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8260 .await
8261 .unwrap();
8262 assert_eq!(sketch_id, ObjectId(1));
8263 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8264 let sketch_object = &scene_delta.new_graph.objects[1];
8265 assert_eq!(sketch_object.id, ObjectId(1));
8266 assert_eq!(
8267 sketch_object.kind,
8268 ObjectKind::Sketch(Sketch {
8269 args: SketchCtor {
8270 on: Plane::Default(PlaneName::Xy),
8271 },
8272 plane: ObjectId(0),
8273 segments: vec![],
8274 constraints: vec![],
8275 })
8276 );
8277 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8278
8279 let arc_ctor = ArcCtor {
8280 start: Point2d {
8281 x: Expr::Var(Number {
8282 value: 0.0,
8283 units: NumericSuffix::Mm,
8284 }),
8285 y: Expr::Var(Number {
8286 value: 0.0,
8287 units: NumericSuffix::Mm,
8288 }),
8289 },
8290 end: Point2d {
8291 x: Expr::Var(Number {
8292 value: 10.0,
8293 units: NumericSuffix::Mm,
8294 }),
8295 y: Expr::Var(Number {
8296 value: 10.0,
8297 units: NumericSuffix::Mm,
8298 }),
8299 },
8300 center: Point2d {
8301 x: Expr::Var(Number {
8302 value: 10.0,
8303 units: NumericSuffix::Mm,
8304 }),
8305 y: Expr::Var(Number {
8306 value: 0.0,
8307 units: NumericSuffix::Mm,
8308 }),
8309 },
8310 direction: None,
8311 construction: None,
8312 };
8313 let segment = SegmentCtor::Arc(arc_ctor);
8314 let (src_delta, scene_delta) = frontend
8315 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8316 .await
8317 .unwrap();
8318 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8319 assert_eq!(
8320 scene_delta.new_objects,
8321 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8322 );
8323 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8324 assert_eq!(scene_object.id.0, i);
8325 }
8326 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8327
8328 let arc = *scene_delta.new_objects.last().unwrap();
8330
8331 let arc_ctor = ArcCtor {
8332 start: Point2d {
8333 x: Expr::Var(Number {
8334 value: 1.0,
8335 units: NumericSuffix::Mm,
8336 }),
8337 y: Expr::Var(Number {
8338 value: 2.0,
8339 units: NumericSuffix::Mm,
8340 }),
8341 },
8342 end: Point2d {
8343 x: Expr::Var(Number {
8344 value: 13.0,
8345 units: NumericSuffix::Mm,
8346 }),
8347 y: Expr::Var(Number {
8348 value: 14.0,
8349 units: NumericSuffix::Mm,
8350 }),
8351 },
8352 center: Point2d {
8353 x: Expr::Var(Number {
8354 value: 13.0,
8355 units: NumericSuffix::Mm,
8356 }),
8357 y: Expr::Var(Number {
8358 value: 2.0,
8359 units: NumericSuffix::Mm,
8360 }),
8361 },
8362 direction: None,
8363 construction: None,
8364 };
8365 let segments = vec![ExistingSegmentCtor {
8366 id: arc,
8367 ctor: SegmentCtor::Arc(arc_ctor),
8368 }];
8369 let (src_delta, scene_delta) = frontend
8370 .edit_segments(&mock_ctx, version, sketch_id, segments)
8371 .await
8372 .unwrap();
8373 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8374 assert_eq!(scene_delta.new_objects, vec![]);
8375 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8376
8377 ctx.close().await;
8378 mock_ctx.close().await;
8379 }
8380
8381 #[tokio::test(flavor = "multi_thread")]
8382 async fn test_new_sketch_add_circle_edit_circle() {
8383 let program = Program::empty();
8384
8385 let mut frontend = FrontendState::new();
8386 frontend.program = program;
8387
8388 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8389 let mock_ctx = ExecutorContext::new_mock(None).await;
8390 let version = Version(0);
8391
8392 let sketch_args = SketchCtor {
8393 on: Plane::Default(PlaneName::Xy),
8394 };
8395 let (_src_delta, _scene_delta, sketch_id) = frontend
8396 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8397 .await
8398 .unwrap();
8399
8400 let circle_ctor = CircleCtor {
8402 start: Point2d {
8403 x: Expr::Var(Number {
8404 value: 5.0,
8405 units: NumericSuffix::Mm,
8406 }),
8407 y: Expr::Var(Number {
8408 value: 0.0,
8409 units: NumericSuffix::Mm,
8410 }),
8411 },
8412 center: Point2d {
8413 x: Expr::Var(Number {
8414 value: 0.0,
8415 units: NumericSuffix::Mm,
8416 }),
8417 y: Expr::Var(Number {
8418 value: 0.0,
8419 units: NumericSuffix::Mm,
8420 }),
8421 },
8422 construction: None,
8423 };
8424 let segment = SegmentCtor::Circle(circle_ctor);
8425 let (src_delta, scene_delta) = frontend
8426 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8427 .await
8428 .unwrap();
8429 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8430 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8432 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8433
8434 let circle = *scene_delta.new_objects.last().unwrap();
8435
8436 let circle_ctor = CircleCtor {
8438 start: Point2d {
8439 x: Expr::Var(Number {
8440 value: 10.0,
8441 units: NumericSuffix::Mm,
8442 }),
8443 y: Expr::Var(Number {
8444 value: 0.0,
8445 units: NumericSuffix::Mm,
8446 }),
8447 },
8448 center: Point2d {
8449 x: Expr::Var(Number {
8450 value: 3.0,
8451 units: NumericSuffix::Mm,
8452 }),
8453 y: Expr::Var(Number {
8454 value: 4.0,
8455 units: NumericSuffix::Mm,
8456 }),
8457 },
8458 construction: None,
8459 };
8460 let segments = vec![ExistingSegmentCtor {
8461 id: circle,
8462 ctor: SegmentCtor::Circle(circle_ctor),
8463 }];
8464 let (src_delta, scene_delta) = frontend
8465 .edit_segments(&mock_ctx, version, sketch_id, segments)
8466 .await
8467 .unwrap();
8468 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8469 assert_eq!(scene_delta.new_objects, vec![]);
8470 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8471
8472 ctx.close().await;
8473 mock_ctx.close().await;
8474 }
8475
8476 #[tokio::test(flavor = "multi_thread")]
8477 async fn test_delete_circle() {
8478 let initial_source = "sketch001 = sketch(on = XY) {
8479 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8480}
8481";
8482
8483 let program = Program::parse(initial_source).unwrap().0.unwrap();
8484 let mut frontend = FrontendState::new();
8485
8486 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8487 let mock_ctx = ExecutorContext::new_mock(None).await;
8488 let version = Version(0);
8489
8490 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8491 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8492 let sketch_id = sketch_object.id;
8493 let sketch = expect_sketch(sketch_object);
8494
8495 assert_eq!(sketch.segments.len(), 3);
8497 let circle_id = sketch.segments[2];
8498
8499 let (src_delta, scene_delta) = frontend
8501 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8502 .await
8503 .unwrap();
8504 insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8505 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8506 let new_sketch = expect_sketch(new_sketch_object);
8507 assert_eq!(new_sketch.segments.len(), 0);
8508
8509 ctx.close().await;
8510 mock_ctx.close().await;
8511 }
8512
8513 #[tokio::test(flavor = "multi_thread")]
8514 async fn test_edit_circle_via_point() {
8515 let initial_source = "sketch001 = sketch(on = XY) {
8516 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8517}
8518";
8519
8520 let program = Program::parse(initial_source).unwrap().0.unwrap();
8521 let mut frontend = FrontendState::new();
8522
8523 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8524 let mock_ctx = ExecutorContext::new_mock(None).await;
8525 let version = Version(0);
8526
8527 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8528 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8529 let sketch_id = sketch_object.id;
8530 let sketch = expect_sketch(sketch_object);
8531
8532 let circle_id = sketch
8534 .segments
8535 .iter()
8536 .copied()
8537 .find(|seg_id| {
8538 matches!(
8539 &frontend.scene_graph.objects[seg_id.0].kind,
8540 ObjectKind::Segment {
8541 segment: Segment::Circle(_)
8542 }
8543 )
8544 })
8545 .expect("Expected a circle segment in sketch");
8546 let circle_object = &frontend.scene_graph.objects[circle_id.0];
8547 let ObjectKind::Segment {
8548 segment: Segment::Circle(circle),
8549 } = &circle_object.kind
8550 else {
8551 panic!("Expected circle segment, got: {:?}", circle_object.kind);
8552 };
8553 let start_point_id = circle.start;
8554
8555 let segments = vec![ExistingSegmentCtor {
8557 id: start_point_id,
8558 ctor: SegmentCtor::Point(PointCtor {
8559 position: Point2d {
8560 x: Expr::Var(Number {
8561 value: 7.0,
8562 units: NumericSuffix::Mm,
8563 }),
8564 y: Expr::Var(Number {
8565 value: 1.0,
8566 units: NumericSuffix::Mm,
8567 }),
8568 },
8569 }),
8570 }];
8571 let (src_delta, _scene_delta) = frontend
8572 .edit_segments(&mock_ctx, version, sketch_id, segments)
8573 .await
8574 .unwrap();
8575 insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8576
8577 ctx.close().await;
8578 mock_ctx.close().await;
8579 }
8580
8581 #[tokio::test(flavor = "multi_thread")]
8582 async fn test_add_line_when_sketch_block_uses_variable() {
8583 let initial_source = "s = sketch(on = XY) {}
8584";
8585
8586 let program = Program::parse(initial_source).unwrap().0.unwrap();
8587
8588 let mut frontend = FrontendState::new();
8589
8590 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8591 let mock_ctx = ExecutorContext::new_mock(None).await;
8592 let version = Version(0);
8593
8594 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8595 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8596 let sketch_id = sketch_object.id;
8597
8598 let line_ctor = LineCtor {
8599 start: Point2d {
8600 x: Expr::Number(Number {
8601 value: 0.0,
8602 units: NumericSuffix::Mm,
8603 }),
8604 y: Expr::Number(Number {
8605 value: 0.0,
8606 units: NumericSuffix::Mm,
8607 }),
8608 },
8609 end: Point2d {
8610 x: Expr::Number(Number {
8611 value: 10.0,
8612 units: NumericSuffix::Mm,
8613 }),
8614 y: Expr::Number(Number {
8615 value: 10.0,
8616 units: NumericSuffix::Mm,
8617 }),
8618 },
8619 construction: None,
8620 };
8621 let segment = SegmentCtor::Line(line_ctor);
8622 let (src_delta, scene_delta) = frontend
8623 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8624 .await
8625 .unwrap();
8626 insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8627 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8628 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8629
8630 ctx.close().await;
8631 mock_ctx.close().await;
8632 }
8633
8634 #[tokio::test(flavor = "multi_thread")]
8635 async fn test_new_sketch_add_line_delete_sketch() {
8636 let program = Program::empty();
8637
8638 let mut frontend = FrontendState::new();
8639 frontend.program = program;
8640
8641 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8642 let mock_ctx = ExecutorContext::new_mock(None).await;
8643 let version = Version(0);
8644
8645 let sketch_args = SketchCtor {
8646 on: Plane::Default(PlaneName::Xy),
8647 };
8648 let (_src_delta, scene_delta, sketch_id) = frontend
8649 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8650 .await
8651 .unwrap();
8652 assert_eq!(sketch_id, ObjectId(1));
8653 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8654 let sketch_object = &scene_delta.new_graph.objects[1];
8655 assert_eq!(sketch_object.id, ObjectId(1));
8656 assert_eq!(
8657 sketch_object.kind,
8658 ObjectKind::Sketch(Sketch {
8659 args: SketchCtor {
8660 on: Plane::Default(PlaneName::Xy)
8661 },
8662 plane: ObjectId(0),
8663 segments: vec![],
8664 constraints: vec![],
8665 })
8666 );
8667 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8668
8669 let line_ctor = LineCtor {
8670 start: Point2d {
8671 x: Expr::Number(Number {
8672 value: 0.0,
8673 units: NumericSuffix::Mm,
8674 }),
8675 y: Expr::Number(Number {
8676 value: 0.0,
8677 units: NumericSuffix::Mm,
8678 }),
8679 },
8680 end: Point2d {
8681 x: Expr::Number(Number {
8682 value: 10.0,
8683 units: NumericSuffix::Mm,
8684 }),
8685 y: Expr::Number(Number {
8686 value: 10.0,
8687 units: NumericSuffix::Mm,
8688 }),
8689 },
8690 construction: None,
8691 };
8692 let segment = SegmentCtor::Line(line_ctor);
8693 let (src_delta, scene_delta) = frontend
8694 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8695 .await
8696 .unwrap();
8697 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8698 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8699
8700 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8701 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8702 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8703
8704 ctx.close().await;
8705 mock_ctx.close().await;
8706 }
8707
8708 #[tokio::test(flavor = "multi_thread")]
8709 async fn test_delete_sketch_when_sketch_block_uses_variable() {
8710 let initial_source = "s = sketch(on = XY) {}
8711";
8712
8713 let program = Program::parse(initial_source).unwrap().0.unwrap();
8714
8715 let mut frontend = FrontendState::new();
8716
8717 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8718 let version = Version(0);
8719
8720 frontend.hack_set_program(&ctx, program).await.unwrap();
8721 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8722 let sketch_id = sketch_object.id;
8723
8724 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8725 insta::assert_snapshot!(
8726 "test_delete_sketch_when_sketch_block_uses_variable",
8727 src_delta.text.as_str()
8728 );
8729 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8730
8731 ctx.close().await;
8732 }
8733
8734 #[tokio::test(flavor = "multi_thread")]
8735 async fn test_delete_sketch_after_comment() {
8736 let initial_source = "sketch001 = sketch(on = XZ) {
8737}
8738";
8739
8740 let program = Program::parse(initial_source).unwrap().0.unwrap();
8741 let mut frontend = FrontendState::new();
8742
8743 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8744 let version = Version(0);
8745
8746 frontend.hack_set_program(&ctx, program).await.unwrap();
8747 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8748 let sketch_id = sketch_object.id;
8749 let original_source = sketch_object.source.clone();
8750
8751 let commented_source = "// test 1
8752sketch001 = sketch(on = XZ) {
8753}
8754";
8755 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8756 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8757
8758 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8759 assert_eq!(cached_sketch_object.source, original_source);
8760
8761 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8762 assert!(
8763 !src_delta.text.contains("sketch001"),
8764 "sketch was not deleted: {}",
8765 src_delta.text
8766 );
8767 insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8769 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8770
8771 ctx.close().await;
8772 }
8773
8774 #[tokio::test(flavor = "multi_thread")]
8775 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8776 let initial_source = "sketch001 = sketch(on = XZ) {
8777}
8778foo = 1
8779";
8780
8781 let program = Program::parse(initial_source).unwrap().0.unwrap();
8782 let mut frontend = FrontendState::new();
8783
8784 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8785 let version = Version(0);
8786
8787 frontend.hack_set_program(&ctx, program).await.unwrap();
8788 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8789 let sketch_id = sketch_object.id;
8790
8791 let commented_source = "// keep me
8792sketch001 = sketch(on = XZ) {
8793}
8794foo = 1
8795";
8796 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8797 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8798
8799 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8800 insta::assert_snapshot!(
8802 "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8803 src_delta.text.as_str()
8804 );
8805
8806 ctx.close().await;
8807 }
8808
8809 #[tokio::test(flavor = "multi_thread")]
8810 async fn test_delete_segment_preserves_pre_comment() {
8811 let initial_source = "\
8812sketch(on = XY) {
8813 point(at = [var 1, var 2])
8814 // describe the middle point
8815 point(at = [var 3, var 4])
8816 point(at = [var 5, var 6])
8817}
8818";
8819
8820 let program = Program::parse(initial_source).unwrap().0.unwrap();
8821 let mut frontend = FrontendState::new();
8822
8823 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8824 let mock_ctx = ExecutorContext::new_mock(None).await;
8825 let version = Version(0);
8826
8827 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8828 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8829 let sketch_id = sketch_object.id;
8830 let sketch = expect_sketch(sketch_object);
8831
8832 let middle_point_id = *sketch.segments.get(1).unwrap();
8833
8834 let (src_delta, _scene_delta) = frontend
8835 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8836 .await
8837 .unwrap();
8838 insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8841
8842 ctx.close().await;
8843 mock_ctx.close().await;
8844 }
8845
8846 #[tokio::test(flavor = "multi_thread")]
8847 async fn test_delete_last_segment_preserves_pre_comment() {
8848 let initial_source = "\
8849sketch(on = XY) {
8850 point(at = [var 1, var 2])
8851 // describe the trailing point
8852 point(at = [var 3, var 4])
8853}
8854";
8855
8856 let program = Program::parse(initial_source).unwrap().0.unwrap();
8857 let mut frontend = FrontendState::new();
8858
8859 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8860 let mock_ctx = ExecutorContext::new_mock(None).await;
8861 let version = Version(0);
8862
8863 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8864 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8865 let sketch_id = sketch_object.id;
8866 let sketch = expect_sketch(sketch_object);
8867
8868 let last_point_id = *sketch.segments.last().unwrap();
8869
8870 let (src_delta, _scene_delta) = frontend
8871 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8872 .await
8873 .unwrap();
8874 insta::assert_snapshot!(
8877 "test_delete_last_segment_preserves_pre_comment",
8878 src_delta.text.as_str()
8879 );
8880
8881 ctx.close().await;
8882 mock_ctx.close().await;
8883 }
8884
8885 #[tokio::test(flavor = "multi_thread")]
8886 async fn test_delete_segment_drops_inline_trailing_comment() {
8887 let initial_source = "\
8888sketch(on = XY) {
8889 point(at = [var 1, var 2])
8890 point(at = [var 3, var 4]) // same-line note that gets dropped
8891 point(at = [var 5, var 6])
8892}
8893";
8894
8895 let program = Program::parse(initial_source).unwrap().0.unwrap();
8896 let mut frontend = FrontendState::new();
8897
8898 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8899 let mock_ctx = ExecutorContext::new_mock(None).await;
8900 let version = Version(0);
8901
8902 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8903 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8904 let sketch_id = sketch_object.id;
8905 let sketch = expect_sketch(sketch_object);
8906
8907 let middle_point_id = *sketch.segments.get(1).unwrap();
8908
8909 let (src_delta, _scene_delta) = frontend
8910 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8911 .await
8912 .unwrap();
8913 assert!(
8915 !src_delta.text.contains("same-line note"),
8916 "inline comment should have been removed: {}",
8917 src_delta.text
8918 );
8919
8920 ctx.close().await;
8921 mock_ctx.close().await;
8922 }
8923
8924 #[tokio::test(flavor = "multi_thread")]
8925 async fn test_delete_segments_preserves_block_comments_across_positions() {
8926 let initial_source = "\
8934sketch(on = XY) {
8935 /* above first - moves to middle */
8936 point(at = [var 1, var 2]) /* same-line on first - dropped */
8937 /* above middle - stays */
8938 point(at = [var 3, var 4])
8939 /* above last - moves to trailing meta */
8940 point(at = [var 5, var 6])
8941}
8942";
8943
8944 let program = Program::parse(initial_source).unwrap().0.unwrap();
8945 let mut frontend = FrontendState::new();
8946
8947 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8948 let mock_ctx = ExecutorContext::new_mock(None).await;
8949 let version = Version(0);
8950
8951 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8952 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8953 let sketch_id = sketch_object.id;
8954 let sketch = expect_sketch(sketch_object);
8955
8956 let first_point_id = *sketch.segments.first().unwrap();
8957 let last_point_id = *sketch.segments.last().unwrap();
8958
8959 let (src_delta, _scene_delta) = frontend
8960 .delete_objects(
8961 &mock_ctx,
8962 version,
8963 sketch_id,
8964 Vec::new(),
8965 vec![first_point_id, last_point_id],
8966 )
8967 .await
8968 .unwrap();
8969 insta::assert_snapshot!(
8970 "test_delete_segments_preserves_block_comments_across_positions",
8971 src_delta.text.as_str()
8972 );
8973
8974 ctx.close().await;
8975 mock_ctx.close().await;
8976 }
8977
8978 #[tokio::test(flavor = "multi_thread")]
8979 async fn test_edit_line_when_editing_its_start_point() {
8980 let initial_source = "\
8981sketch(on = XY) {
8982 line(start = [var 1, var 2], end = [var 3, var 4])
8983}
8984";
8985
8986 let program = Program::parse(initial_source).unwrap().0.unwrap();
8987
8988 let mut frontend = FrontendState::new();
8989
8990 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8991 let mock_ctx = ExecutorContext::new_mock(None).await;
8992 let version = Version(0);
8993
8994 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8995 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8996 let sketch_id = sketch_object.id;
8997 let sketch = expect_sketch(sketch_object);
8998
8999 let point_id = *sketch.segments.first().unwrap();
9000
9001 let point_ctor = PointCtor {
9002 position: Point2d {
9003 x: Expr::Var(Number {
9004 value: 5.0,
9005 units: NumericSuffix::Inch,
9006 }),
9007 y: Expr::Var(Number {
9008 value: 6.0,
9009 units: NumericSuffix::Inch,
9010 }),
9011 },
9012 };
9013 let segments = vec![ExistingSegmentCtor {
9014 id: point_id,
9015 ctor: SegmentCtor::Point(point_ctor),
9016 }];
9017 let (src_delta, scene_delta) = frontend
9018 .edit_segments(&mock_ctx, version, sketch_id, segments)
9019 .await
9020 .unwrap();
9021 insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
9022 assert_eq!(scene_delta.new_objects, vec![]);
9023 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9024
9025 ctx.close().await;
9026 mock_ctx.close().await;
9027 }
9028
9029 #[tokio::test(flavor = "multi_thread")]
9030 async fn test_edit_line_when_editing_its_end_point() {
9031 let initial_source = "\
9032sketch(on = XY) {
9033 line(start = [var 1, var 2], end = [var 3, var 4])
9034}
9035";
9036
9037 let program = Program::parse(initial_source).unwrap().0.unwrap();
9038
9039 let mut frontend = FrontendState::new();
9040
9041 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9042 let mock_ctx = ExecutorContext::new_mock(None).await;
9043 let version = Version(0);
9044
9045 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9046 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9047 let sketch_id = sketch_object.id;
9048 let sketch = expect_sketch(sketch_object);
9049 let point_id = *sketch.segments.get(1).unwrap();
9050
9051 let point_ctor = PointCtor {
9052 position: Point2d {
9053 x: Expr::Var(Number {
9054 value: 5.0,
9055 units: NumericSuffix::Inch,
9056 }),
9057 y: Expr::Var(Number {
9058 value: 6.0,
9059 units: NumericSuffix::Inch,
9060 }),
9061 },
9062 };
9063 let segments = vec![ExistingSegmentCtor {
9064 id: point_id,
9065 ctor: SegmentCtor::Point(point_ctor),
9066 }];
9067 let (src_delta, scene_delta) = frontend
9068 .edit_segments(&mock_ctx, version, sketch_id, segments)
9069 .await
9070 .unwrap();
9071 insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9072 assert_eq!(scene_delta.new_objects, vec![]);
9073 assert_eq!(
9074 scene_delta.new_graph.objects.len(),
9075 5,
9076 "{:#?}",
9077 scene_delta.new_graph.objects
9078 );
9079
9080 ctx.close().await;
9081 mock_ctx.close().await;
9082 }
9083
9084 #[tokio::test(flavor = "multi_thread")]
9085 async fn test_edit_line_with_coincident_feedback() {
9086 let initial_source = "\
9087sketch(on = XY) {
9088 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9089 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9090 fixed([line1.start, [0, 0]])
9091 coincident([line1.end, line2.start])
9092 equalLength([line1, line2])
9093}
9094";
9095
9096 let program = Program::parse(initial_source).unwrap().0.unwrap();
9097
9098 let mut frontend = FrontendState::new();
9099
9100 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9101 let mock_ctx = ExecutorContext::new_mock(None).await;
9102 let version = Version(0);
9103
9104 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9105 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9106 let sketch_id = sketch_object.id;
9107 let sketch = expect_sketch(sketch_object);
9108 let line2_end_id = *sketch.segments.get(4).unwrap();
9109
9110 let segments = vec![ExistingSegmentCtor {
9111 id: line2_end_id,
9112 ctor: SegmentCtor::Point(PointCtor {
9113 position: Point2d {
9114 x: Expr::Var(Number {
9115 value: 9.0,
9116 units: NumericSuffix::None,
9117 }),
9118 y: Expr::Var(Number {
9119 value: 10.0,
9120 units: NumericSuffix::None,
9121 }),
9122 },
9123 }),
9124 }];
9125 let (src_delta, scene_delta) = frontend
9126 .edit_segments(&mock_ctx, version, sketch_id, segments)
9127 .await
9128 .unwrap();
9129 insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9130 assert_eq!(
9131 scene_delta.new_graph.objects.len(),
9132 11,
9133 "{:#?}",
9134 scene_delta.new_graph.objects
9135 );
9136
9137 ctx.close().await;
9138 mock_ctx.close().await;
9139 }
9140
9141 #[tokio::test(flavor = "multi_thread")]
9142 async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9143 let initial_source = "\
9144sketch(on = XY) {
9145 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9146 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9147 fixed([line1.start, [0, 0]])
9148 coincident([line1.end, line2.start])
9149 equalLength([line1, line2])
9150}
9151";
9152
9153 let program = Program::parse(initial_source).unwrap().0.unwrap();
9154 let mut frontend = FrontendState::new();
9155 let mock_ctx = ExecutorContext::new_mock(None).await;
9156 let version = Version(0);
9157
9158 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9159 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9160 let sketch_id = sketch_object.id;
9161 let sketch = expect_sketch(sketch_object);
9162 let line2_end_id = *sketch.segments.get(4).unwrap();
9163
9164 let segments = vec![ExistingSegmentCtor {
9165 id: line2_end_id,
9166 ctor: SegmentCtor::Point(PointCtor {
9167 position: Point2d {
9168 x: Expr::Var(Number {
9169 value: 9.0,
9170 units: NumericSuffix::None,
9171 }),
9172 y: Expr::Var(Number {
9173 value: 10.0,
9174 units: NumericSuffix::None,
9175 }),
9176 },
9177 }),
9178 }];
9179 let (edited_source, _) = frontend
9180 .edit_segments(&mock_ctx, version, sketch_id, segments)
9181 .await
9182 .unwrap();
9183
9184 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9185 assert_eq!(mock_source.text, edited_source.text);
9186
9187 mock_ctx.close().await;
9188 }
9189
9190 #[tokio::test(flavor = "multi_thread")]
9193 async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9194 let initial_source = "\
9195sketch(on = XY) {
9196 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9197 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9198 fixed([line1.start, [0, 0]])
9199 coincident([line1.end, line2.start])
9200 equalLength([line1, line2])
9201}
9202";
9203
9204 let program = Program::parse(initial_source).unwrap().0.unwrap();
9205 let mut frontend = FrontendState::new();
9206 let mock_ctx = ExecutorContext::new_mock(None).await;
9207 let version = Version(0);
9208
9209 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9210 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9211 let sketch_id = sketch_object.id;
9212 let sketch = expect_sketch(sketch_object);
9213 let line2_end_id = *sketch.segments.get(4).unwrap();
9214
9215 let segments = vec![ExistingSegmentCtor {
9216 id: line2_end_id,
9217 ctor: SegmentCtor::Point(PointCtor {
9218 position: Point2d {
9219 x: Expr::Var(Number {
9220 value: 9.0,
9221 units: NumericSuffix::None,
9222 }),
9223 y: Expr::Var(Number {
9224 value: 10.0,
9225 units: NumericSuffix::None,
9226 }),
9227 },
9228 }),
9229 }];
9230 let (preview_source, preview_delta) = frontend
9231 .edit_segments_with_options(
9232 &mock_ctx,
9233 version,
9234 sketch_id,
9235 segments,
9236 EditSegmentsOptions {
9237 anchor_segment_ids: Some(vec![line2_end_id]),
9238 drag_anchors: Vec::new(),
9239 constraint_label_edits: Vec::new(),
9240 commit_solved_initial_guesses: false,
9241 },
9242 )
9243 .await
9244 .unwrap();
9245
9246 assert!(
9247 !preview_delta.exec_outcome.var_solutions.is_empty(),
9248 "preview solve should still solve and return geometry feedback"
9249 );
9250 assert!(
9251 preview_source
9252 .text
9253 .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9254 );
9255 assert!(
9256 preview_source
9257 .text
9258 .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9259 );
9260
9261 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9262 assert_eq!(mock_source.text, preview_source.text);
9263
9264 mock_ctx.close().await;
9265 }
9266
9267 #[tokio::test(flavor = "multi_thread")]
9268 async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9269 let initial_source = "\
9270sketch(on = XY) {
9271 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9272}
9273";
9274
9275 let program = Program::parse(initial_source).unwrap().0.unwrap();
9276 let mut frontend = FrontendState::new();
9277 let mock_ctx = ExecutorContext::new_mock(None).await;
9278 let version = Version(0);
9279
9280 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9281 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9282 let sketch_id = sketch_object.id;
9283 let sketch = expect_sketch(sketch_object);
9284 let line_end_id = *sketch.segments.get(1).unwrap();
9285
9286 let constraint = Constraint::Fixed(Fixed {
9287 points: vec![FixedPoint {
9288 point: line_end_id,
9289 position: Point2d {
9290 x: Number {
9291 value: 20.0,
9292 units: NumericSuffix::Mm,
9293 },
9294 y: Number {
9295 value: 0.0,
9296 units: NumericSuffix::Mm,
9297 },
9298 },
9299 }],
9300 });
9301 let (constraint_source, _) = frontend
9302 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9303 .await
9304 .unwrap();
9305
9306 assert!(
9307 constraint_source
9308 .text
9309 .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9310 "{}",
9311 constraint_source.text
9312 );
9313 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9314 assert_eq!(mock_source.text, constraint_source.text);
9315
9316 mock_ctx.close().await;
9317 }
9318
9319 #[test]
9320 fn test_no_solver_feedback_preserves_original_source() {
9321 let initial_source = "\
9322@settings(defaultLengthUnit = in, kclVersion = 2.0)
9323cylinder = startSketchOn(XY)
9324 |> circle(center= [0, 0], radius= 22)
9325 |> extrude(length = 14)
9326";
9327 let mut frontend = FrontendState::new();
9328 frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9329 let outcome = ExecOutcome {
9330 variables: Default::default(),
9331 operations: Default::default(),
9332 artifact_graph: Default::default(),
9333 scene_objects: Default::default(),
9334 source_range_to_object: Default::default(),
9335 var_solutions: Default::default(),
9336 refactor_metadata: Default::default(),
9337 issues: Default::default(),
9338 filenames: Default::default(),
9339 source_files: Default::default(),
9340 default_planes: Default::default(),
9341 };
9342
9343 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9344
9345 assert_eq!(source_delta.text, initial_source);
9346 }
9347
9348 #[tokio::test(flavor = "multi_thread")]
9351 async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9352 let initial_source = "\
9353sketch(on = XY) {
9354 point1 = point(at = [var 0mm, var 0mm])
9355 point2 = point(at = [var 0mm, var 0mm])
9356 coincident([point1, point2])
9357}
9358";
9359
9360 let program = Program::parse(initial_source).unwrap().0.unwrap();
9361 let mut frontend = FrontendState::new();
9362 let mock_ctx = ExecutorContext::new_mock(None).await;
9363 let version = Version(0);
9364
9365 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9366 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9367 let sketch_id = sketch_object.id;
9368 let sketch = expect_sketch(sketch_object);
9369 let point1_id = sketch.segments[0];
9370 let point2_id = sketch.segments[1];
9371
9372 let segments = vec![
9373 ExistingSegmentCtor {
9374 id: point1_id,
9375 ctor: SegmentCtor::Point(PointCtor {
9376 position: point_expr_mm(10.0, 0.0),
9377 }),
9378 },
9379 ExistingSegmentCtor {
9380 id: point2_id,
9381 ctor: SegmentCtor::Point(PointCtor {
9382 position: point_expr_mm(100.0, 0.0),
9383 }),
9384 },
9385 ];
9386 let (_, scene_delta) = frontend
9387 .edit_segments_with_options(
9388 &mock_ctx,
9389 version,
9390 sketch_id,
9391 segments,
9392 EditSegmentsOptions {
9393 anchor_segment_ids: Some(vec![point1_id]),
9394 drag_anchors: Vec::new(),
9395 constraint_label_edits: Vec::new(),
9396 commit_solved_initial_guesses: true,
9397 },
9398 )
9399 .await
9400 .unwrap();
9401
9402 assert_point_position_close(
9403 point_position(&scene_delta.new_graph, point1_id),
9404 point_number_mm(10.0, 0.0),
9405 );
9406 assert_point_position_close(
9407 point_position(&scene_delta.new_graph, point2_id),
9408 point_number_mm(10.0, 0.0),
9409 );
9410
9411 mock_ctx.close().await;
9412 }
9413
9414 fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9417 use std::cell::RefCell;
9418 struct Collector {
9419 target: f64,
9420 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9421 }
9422 impl<'a> crate::walk::Visitor<'a> for &Collector {
9423 type Error = crate::front::Error;
9424 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9425 if let crate::walk::Node::SketchVar(sketch_var) = node
9426 && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9427 && (initial.value - self.target).abs() < 1e-9
9428 {
9429 self.out
9430 .borrow_mut()
9431 .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9432 }
9433 for child in node.children().iter() {
9434 if !child.visit(*self)? {
9435 return Ok(false);
9436 }
9437 }
9438 Ok(true)
9439 }
9440 }
9441 let collector = Collector {
9442 target: value,
9443 out: Default::default(),
9444 };
9445 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9446 collector.out.into_inner()
9447 }
9448
9449 fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9452 use std::cell::RefCell;
9453 struct Collector {
9454 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9455 }
9456 impl<'a> crate::walk::Visitor<'a> for &Collector {
9457 type Error = crate::front::Error;
9458 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9459 if let crate::walk::Node::SketchVar(sketch_var) = node
9460 && let Some(node_path) = &sketch_var.node_path
9461 {
9462 self.out
9463 .borrow_mut()
9464 .push((SourceRange::from(sketch_var), node_path.clone()));
9465 }
9466 for child in node.children().iter() {
9467 if !child.visit(*self)? {
9468 return Ok(false);
9469 }
9470 }
9471 Ok(true)
9472 }
9473 }
9474 let collector = Collector {
9475 out: Default::default(),
9476 };
9477 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9478 collector.out.into_inner()
9479 }
9480
9481 fn empty_exec_outcome_with_var_solutions(
9482 var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9483 ) -> ExecOutcome {
9484 ExecOutcome {
9485 variables: Default::default(),
9486 operations: Default::default(),
9487 artifact_graph: Default::default(),
9488 scene_objects: Default::default(),
9489 source_range_to_object: Default::default(),
9490 var_solutions,
9491 refactor_metadata: Default::default(),
9492 issues: Default::default(),
9493 filenames: Default::default(),
9494 source_files: Default::default(),
9495 default_planes: Default::default(),
9496 }
9497 }
9498
9499 #[test]
9503 fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9504 let initial_source = "\
9505sketch(on = XY) {
9506 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9507}
9508";
9509 let program = Program::parse(initial_source).unwrap().0.unwrap();
9510 let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9511 assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9512 let (literal_range, node_path) = matches.into_iter().next().unwrap();
9513
9514 let mut frontend = FrontendState::new();
9515 frontend.program = program;
9516
9517 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9518 literal_range,
9519 Some(node_path),
9520 Number {
9521 value: 25.0,
9522 units: NumericSuffix::Mm,
9523 },
9524 )]);
9525
9526 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9527
9528 insta::assert_snapshot!(
9529 "test_commit_var_solution_by_node_path_updates_sketch_var",
9530 source_delta.text
9531 );
9532 }
9533
9534 #[test]
9541 fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9542 let compact_source = "\
9543sketch(on = XY) {
9544 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9545}
9546";
9547 let padded_source = "\
9548// added comment\n// added comment\n\nsketch(on = XY) {
9549 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9550}
9551";
9552 let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9553 let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9554
9555 let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9556 .into_iter()
9557 .next()
9558 .expect("expected `var 10mm` in compact source");
9559 let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9560 .into_iter()
9561 .next()
9562 .expect("expected `var 10mm` in padded source");
9563
9564 assert_ne!(
9565 compact_match.0, padded_match.0,
9566 "byte offsets must differ for this test to be meaningful"
9567 );
9568 assert_eq!(
9569 compact_match.1, padded_match.1,
9570 "node paths must agree across whitespace; that's the whole point of NodePath",
9571 );
9572
9573 let mut frontend = FrontendState::new();
9574 frontend.program = padded_program;
9575
9576 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9578 compact_match.0,
9579 Some(compact_match.1),
9580 Number {
9581 value: 30.0,
9582 units: NumericSuffix::Mm,
9583 },
9584 )]);
9585
9586 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9587
9588 insta::assert_snapshot!(
9589 "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9590 source_delta.text
9591 );
9592 }
9593
9594 #[test]
9598 fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9599 let initial_source = "\
9600sketch(on = XY) {
9601 line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9602}
9603";
9604 let program = Program::parse(initial_source).unwrap().0.unwrap();
9605
9606 let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9607 .into_iter()
9608 .next()
9609 .expect("expected `var 10mm`");
9610 let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9611 .into_iter()
9612 .next()
9613 .expect("expected `var 20mm`");
9614
9615 let mut frontend = FrontendState::new();
9616 frontend.program = program;
9617
9618 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9620 var_20.0,
9621 Some(var_10.1),
9622 Number {
9623 value: 33.0,
9624 units: NumericSuffix::Mm,
9625 },
9626 )]);
9627
9628 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9629
9630 insta::assert_snapshot!(
9631 "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9632 source_delta.text
9633 );
9634 }
9635
9636 #[test]
9643 fn test_commit_var_solution_writes_back_into_bare_var() {
9644 let initial_source = "\
9645@settings(experimentalFeatures = allow, kclVersion = 2.0)
9646sketch(on = XY) {
9647 line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9648}
9649";
9650 let program = Program::parse(initial_source).unwrap().0.unwrap();
9651
9652 let bare = collect_all_sketch_vars(&program)
9655 .into_iter()
9656 .find(|(range, _)| {
9657 range.end() - range.start() == 3
9659 })
9660 .expect("expected at least one bare `var`");
9661
9662 let mut frontend = FrontendState::new();
9663 frontend.program = program;
9664
9665 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9666 bare.0,
9667 Some(bare.1),
9668 Number {
9669 value: 7.0,
9670 units: NumericSuffix::Mm,
9671 },
9672 )]);
9673
9674 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9675
9676 insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9680 }
9681
9682 #[tokio::test(flavor = "multi_thread")]
9683 async fn test_delete_point_without_var() {
9684 let initial_source = "\
9685sketch(on = XY) {
9686 point(at = [var 1, var 2])
9687 point(at = [var 3, var 4])
9688 point(at = [var 5, var 6])
9689}
9690";
9691
9692 let program = Program::parse(initial_source).unwrap().0.unwrap();
9693
9694 let mut frontend = FrontendState::new();
9695
9696 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9697 let mock_ctx = ExecutorContext::new_mock(None).await;
9698 let version = Version(0);
9699
9700 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9701 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9702 let sketch_id = sketch_object.id;
9703 let sketch = expect_sketch(sketch_object);
9704
9705 let point_id = *sketch.segments.get(1).unwrap();
9706
9707 let (src_delta, scene_delta) = frontend
9708 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9709 .await
9710 .unwrap();
9711 insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9712 assert_eq!(scene_delta.new_objects, vec![]);
9713 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9714
9715 ctx.close().await;
9716 mock_ctx.close().await;
9717 }
9718
9719 #[tokio::test(flavor = "multi_thread")]
9720 async fn test_delete_point_with_var() {
9721 let initial_source = "\
9722sketch(on = XY) {
9723 point(at = [var 1, var 2])
9724 point1 = point(at = [var 3, var 4])
9725 point(at = [var 5, var 6])
9726}
9727";
9728
9729 let program = Program::parse(initial_source).unwrap().0.unwrap();
9730
9731 let mut frontend = FrontendState::new();
9732
9733 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9734 let mock_ctx = ExecutorContext::new_mock(None).await;
9735 let version = Version(0);
9736
9737 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9738 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9739 let sketch_id = sketch_object.id;
9740 let sketch = expect_sketch(sketch_object);
9741
9742 let point_id = *sketch.segments.get(1).unwrap();
9743
9744 let (src_delta, scene_delta) = frontend
9745 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9746 .await
9747 .unwrap();
9748 insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9749 assert_eq!(scene_delta.new_objects, vec![]);
9750 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9751
9752 ctx.close().await;
9753 mock_ctx.close().await;
9754 }
9755
9756 #[tokio::test(flavor = "multi_thread")]
9757 async fn test_delete_multiple_points() {
9758 let initial_source = "\
9759sketch(on = XY) {
9760 point(at = [var 1, var 2])
9761 point1 = point(at = [var 3, var 4])
9762 point(at = [var 5, var 6])
9763}
9764";
9765
9766 let program = Program::parse(initial_source).unwrap().0.unwrap();
9767
9768 let mut frontend = FrontendState::new();
9769
9770 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9771 let mock_ctx = ExecutorContext::new_mock(None).await;
9772 let version = Version(0);
9773
9774 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9775 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9776 let sketch_id = sketch_object.id;
9777
9778 let sketch = expect_sketch(sketch_object);
9779
9780 let point1_id = *sketch.segments.first().unwrap();
9781 let point2_id = *sketch.segments.get(1).unwrap();
9782
9783 let (src_delta, scene_delta) = frontend
9784 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9785 .await
9786 .unwrap();
9787 insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9788 assert_eq!(scene_delta.new_objects, vec![]);
9789 assert_eq!(scene_delta.new_graph.objects.len(), 3);
9790
9791 ctx.close().await;
9792 mock_ctx.close().await;
9793 }
9794
9795 #[tokio::test(flavor = "multi_thread")]
9796 async fn test_delete_coincident_constraint() {
9797 let initial_source = "\
9798sketch(on = XY) {
9799 point1 = point(at = [var 1, var 2])
9800 point2 = point(at = [var 3, var 4])
9801 coincident([point1, point2])
9802 point(at = [var 5, var 6])
9803}
9804";
9805
9806 let program = Program::parse(initial_source).unwrap().0.unwrap();
9807
9808 let mut frontend = FrontendState::new();
9809
9810 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9811 let mock_ctx = ExecutorContext::new_mock(None).await;
9812 let version = Version(0);
9813
9814 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9815 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9816 let sketch_id = sketch_object.id;
9817 let sketch = expect_sketch(sketch_object);
9818
9819 let coincident_id = *sketch.constraints.first().unwrap();
9820
9821 let (src_delta, scene_delta) = frontend
9822 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9823 .await
9824 .unwrap();
9825 insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9826 assert_eq!(scene_delta.new_objects, vec![]);
9827 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9828
9829 ctx.close().await;
9830 mock_ctx.close().await;
9831 }
9832
9833 #[tokio::test(flavor = "multi_thread")]
9834 async fn test_delete_line_cascades_to_coincident_constraint() {
9835 let initial_source = "\
9836sketch(on = XY) {
9837 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9838 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9839 coincident([line1.end, line2.start])
9840}
9841";
9842
9843 let program = Program::parse(initial_source).unwrap().0.unwrap();
9844
9845 let mut frontend = FrontendState::new();
9846
9847 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9848 let mock_ctx = ExecutorContext::new_mock(None).await;
9849 let version = Version(0);
9850
9851 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9852 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9853 let sketch_id = sketch_object.id;
9854 let sketch = expect_sketch(sketch_object);
9855 let line_id = *sketch.segments.get(5).unwrap();
9856
9857 let (src_delta, scene_delta) = frontend
9858 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9859 .await
9860 .unwrap();
9861 insta::assert_snapshot!(
9862 "test_delete_line_cascades_to_coincident_constraint",
9863 src_delta.text.as_str()
9864 );
9865 assert_eq!(
9866 scene_delta.new_graph.objects.len(),
9867 5,
9868 "{:#?}",
9869 scene_delta.new_graph.objects
9870 );
9871
9872 ctx.close().await;
9873 mock_ctx.close().await;
9874 }
9875
9876 #[tokio::test(flavor = "multi_thread")]
9877 async fn test_delete_line_cascades_to_distance_constraint() {
9878 let initial_source = "\
9879sketch(on = XY) {
9880 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9881 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9882 distance([line1.end, line2.start]) == 10mm
9883}
9884";
9885
9886 let program = Program::parse(initial_source).unwrap().0.unwrap();
9887
9888 let mut frontend = FrontendState::new();
9889
9890 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9891 let mock_ctx = ExecutorContext::new_mock(None).await;
9892 let version = Version(0);
9893
9894 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9895 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9896 let sketch_id = sketch_object.id;
9897 let sketch = expect_sketch(sketch_object);
9898 let line_id = *sketch.segments.get(5).unwrap();
9899
9900 let (src_delta, scene_delta) = frontend
9901 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9902 .await
9903 .unwrap();
9904 insta::assert_snapshot!(
9905 "test_delete_line_cascades_to_distance_constraint",
9906 src_delta.text.as_str()
9907 );
9908 assert_eq!(
9909 scene_delta.new_graph.objects.len(),
9910 5,
9911 "{:#?}",
9912 scene_delta.new_graph.objects
9913 );
9914
9915 ctx.close().await;
9916 mock_ctx.close().await;
9917 }
9918
9919 #[tokio::test(flavor = "multi_thread")]
9920 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9921 let initial_source = "\
9922sketch(on = XY) {
9923 point1 = point(at = [var 1, var 2])
9924 point2 = point(at = [var 3, var 4])
9925 horizontalDistance([point1, point2]) == 10mm
9926}
9927";
9928
9929 let program = Program::parse(initial_source).unwrap().0.unwrap();
9930
9931 let mut frontend = FrontendState::new();
9932
9933 let mock_ctx = ExecutorContext::new_mock(None).await;
9934 let version = Version(0);
9935
9936 frontend.program = program.clone();
9937 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9938 frontend.update_state_after_exec(outcome, true);
9939 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9940 let sketch_id = sketch_object.id;
9941 let sketch = expect_sketch(sketch_object);
9942 let point2_id = *sketch.segments.get(1).unwrap();
9943
9944 let (src_delta, scene_delta) = frontend
9945 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9946 .await
9947 .unwrap();
9948 insta::assert_snapshot!(
9949 "test_delete_point_cascades_to_horizontal_distance_constraint",
9950 src_delta.text.as_str()
9951 );
9952 assert_eq!(
9953 scene_delta.new_graph.objects.len(),
9954 3,
9955 "{:#?}",
9956 scene_delta.new_graph.objects
9957 );
9958
9959 mock_ctx.close().await;
9960 }
9961
9962 #[tokio::test(flavor = "multi_thread")]
9963 async fn test_delete_line_cascades_to_fixed_constraint() {
9964 let initial_source = "\
9965sketch(on = XY) {
9966 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9967 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9968 fixed([line1.start, [0, 0]])
9969}
9970";
9971
9972 let program = Program::parse(initial_source).unwrap().0.unwrap();
9973
9974 let mut frontend = FrontendState::new();
9975
9976 let mock_ctx = ExecutorContext::new_mock(None).await;
9977 let version = Version(0);
9978
9979 frontend.program = program.clone();
9980 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9981 frontend.update_state_after_exec(outcome, true);
9982 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9983 let sketch_id = sketch_object.id;
9984 let sketch = expect_sketch(sketch_object);
9985 let line1_id = *sketch.segments.get(2).unwrap();
9986
9987 let (src_delta, scene_delta) = frontend
9988 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9989 .await
9990 .unwrap();
9991 insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9992 assert_eq!(
9993 scene_delta.new_graph.objects.len(),
9994 5,
9995 "{:#?}",
9996 scene_delta.new_graph.objects
9997 );
9998
9999 mock_ctx.close().await;
10000 }
10001
10002 #[tokio::test(flavor = "multi_thread")]
10003 async fn test_delete_line_cascades_to_midpoint_constraint() {
10004 let initial_source = "\
10005sketch(on = XY) {
10006 point1 = point(at = [var 1, var 2])
10007 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
10008 midpoint(line1, point = point1)
10009}
10010";
10011
10012 let program = Program::parse(initial_source).unwrap().0.unwrap();
10013
10014 let mut frontend = FrontendState::new();
10015
10016 let mock_ctx = ExecutorContext::new_mock(None).await;
10017 let version = Version(0);
10018
10019 frontend.program = program.clone();
10020 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10021 frontend.update_state_after_exec(outcome, true);
10022 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10023 let sketch_id = sketch_object.id;
10024 let sketch = expect_sketch(sketch_object);
10025 let line1_id = *sketch.segments.get(3).unwrap();
10026
10027 let (src_delta, scene_delta) = frontend
10028 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10029 .await
10030 .unwrap();
10031 insta::assert_snapshot!(
10032 "test_delete_line_cascades_to_midpoint_constraint",
10033 src_delta.text.as_str()
10034 );
10035 assert_eq!(
10036 scene_delta.new_graph.objects.len(),
10037 3,
10038 "{:#?}",
10039 scene_delta.new_graph.objects
10040 );
10041
10042 mock_ctx.close().await;
10043 }
10044
10045 #[tokio::test(flavor = "multi_thread")]
10046 async fn test_delete_point_preserves_multiline_coincident_constraint() {
10047 let initial_source = "\
10048sketch(on = XY) {
10049 point1 = point(at = [var 1, var 2])
10050 point2 = point(at = [var 3, var 4])
10051 point3 = point(at = [var 5, var 6])
10052 coincident([point1, point2, point3])
10053}
10054";
10055
10056 let program = Program::parse(initial_source).unwrap().0.unwrap();
10057
10058 let mut frontend = FrontendState::new();
10059
10060 let mock_ctx = ExecutorContext::new_mock(None).await;
10061 let version = Version(0);
10062
10063 frontend.program = program.clone();
10064 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10065 frontend.update_state_after_exec(outcome, true);
10066 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10067 let sketch_id = sketch_object.id;
10068 let sketch = expect_sketch(sketch_object);
10069 let point3_id = *sketch.segments.get(2).unwrap();
10070
10071 let (src_delta, scene_delta) = frontend
10072 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10073 .await
10074 .unwrap();
10075 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10076 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10077 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10078 assert!(
10079 src_delta.text.contains("coincident([point1, point2])"),
10080 "{}",
10081 src_delta.text
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.segments.len(), 2);
10087 assert_eq!(sketch.constraints.len(), 1);
10088
10089 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10090 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10091 panic!("Expected constraint object");
10092 };
10093 let Constraint::Coincident(coincident) = constraint else {
10094 panic!("Expected coincident constraint");
10095 };
10096 assert_eq!(
10097 coincident.segments,
10098 sketch
10099 .segments
10100 .iter()
10101 .copied()
10102 .map(Into::into)
10103 .collect::<Vec<ConstraintSegment>>()
10104 );
10105
10106 mock_ctx.close().await;
10107 }
10108
10109 #[tokio::test(flavor = "multi_thread")]
10110 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10111 let initial_source = "\
10112sketch(on = XY) {
10113 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10114 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10115 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10116 equalLength([line1, line2, line3])
10117}
10118";
10119
10120 let program = Program::parse(initial_source).unwrap().0.unwrap();
10121
10122 let mut frontend = FrontendState::new();
10123
10124 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10125 let mock_ctx = ExecutorContext::new_mock(None).await;
10126 let version = Version(0);
10127
10128 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10129 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10130 let sketch_id = sketch_object.id;
10131 let sketch = expect_sketch(sketch_object);
10132 let line3_id = *sketch.segments.get(8).unwrap();
10133
10134 let (src_delta, scene_delta) = frontend
10135 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10136 .await
10137 .unwrap();
10138 insta::assert_snapshot!(
10139 "test_delete_line_preserves_multiline_equal_length_constraint",
10140 src_delta.text.as_str()
10141 );
10142
10143 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10144 let sketch = expect_sketch(sketch_object);
10145 assert_eq!(sketch.constraints.len(), 1);
10146
10147 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10148 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10149 panic!("Expected constraint object");
10150 };
10151 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10152 panic!("Expected lines equal length constraint");
10153 };
10154 assert_eq!(lines_equal_length.lines.len(), 2);
10155
10156 ctx.close().await;
10157 mock_ctx.close().await;
10158 }
10159
10160 #[tokio::test(flavor = "multi_thread")]
10161 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10162 let initial_source = "\
10163sketch(on = XY) {
10164 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10165 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10166 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10167 horizontal([line1.end, line2.start, line3.start])
10168}
10169";
10170
10171 let program = Program::parse(initial_source).unwrap().0.unwrap();
10172
10173 let mut frontend = FrontendState::new();
10174
10175 let mock_ctx = ExecutorContext::new_mock(None).await;
10176 let version = Version(0);
10177
10178 frontend.program = program.clone();
10179 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10180 frontend.update_state_after_exec(outcome, true);
10181 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10182 let sketch_id = sketch_object.id;
10183 let sketch = expect_sketch(sketch_object);
10184 let line1_id = *sketch.segments.get(2).unwrap();
10185
10186 let (src_delta, scene_delta) = frontend
10187 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10188 .await
10189 .unwrap();
10190 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10191 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10192 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10193 assert!(
10194 src_delta.text.contains("horizontal([line2.start, line3.start])"),
10195 "{}",
10196 src_delta.text
10197 );
10198
10199 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10200 let sketch = expect_sketch(sketch_object);
10201 assert_eq!(sketch.constraints.len(), 1);
10202
10203 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10204 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10205 panic!("Expected constraint object");
10206 };
10207 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10208 panic!("Expected horizontal points constraint");
10209 };
10210 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10211 assert_eq!(*points, remaining_points);
10212
10213 mock_ctx.close().await;
10214 }
10215
10216 #[tokio::test(flavor = "multi_thread")]
10217 async fn test_delete_line_preserves_multiline_vertical_constraint() {
10218 let initial_source = "\
10219sketch(on = XY) {
10220 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10221 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10222 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10223 vertical([line1.end, line2.start, line3.start])
10224}
10225";
10226
10227 let program = Program::parse(initial_source).unwrap().0.unwrap();
10228
10229 let mut frontend = FrontendState::new();
10230
10231 let mock_ctx = ExecutorContext::new_mock(None).await;
10232 let version = Version(0);
10233
10234 frontend.program = program.clone();
10235 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10236 frontend.update_state_after_exec(outcome, true);
10237 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10238 let sketch_id = sketch_object.id;
10239 let sketch = expect_sketch(sketch_object);
10240 let line1_id = *sketch.segments.get(2).unwrap();
10241
10242 let (src_delta, scene_delta) = frontend
10243 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10244 .await
10245 .unwrap();
10246 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10247 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10248 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10249 assert!(
10250 src_delta.text.contains("vertical([line2.start, line3.start])"),
10251 "{}",
10252 src_delta.text
10253 );
10254
10255 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10256 let sketch = expect_sketch(sketch_object);
10257 assert_eq!(sketch.constraints.len(), 1);
10258
10259 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10260 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10261 panic!("Expected constraint object");
10262 };
10263 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10264 panic!("Expected vertical points constraint");
10265 };
10266 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10267 assert_eq!(*points, remaining_points);
10268
10269 mock_ctx.close().await;
10270 }
10271
10272 #[tokio::test(flavor = "multi_thread")]
10273 async fn test_delete_line_preserves_multiline_coincident_constraint() {
10274 let initial_source = "\
10275sketch(on = XY) {
10276 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10277 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10278 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10279 coincident([line1.end, line2.start, line3.start])
10280}
10281";
10282
10283 let program = Program::parse(initial_source).unwrap().0.unwrap();
10284
10285 let mut frontend = FrontendState::new();
10286
10287 let mock_ctx = ExecutorContext::new_mock(None).await;
10288 let version = Version(0);
10289
10290 frontend.program = program.clone();
10291 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10292 frontend.update_state_after_exec(outcome, true);
10293 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10294 let sketch_id = sketch_object.id;
10295 let sketch = expect_sketch(sketch_object);
10296 let line1_id = *sketch.segments.get(2).unwrap();
10297
10298 let (src_delta, scene_delta) = frontend
10299 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10300 .await
10301 .unwrap();
10302 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10303 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10304 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10305 assert!(
10306 src_delta.text.contains("coincident([line2.start, line3.start])"),
10307 "{}",
10308 src_delta.text
10309 );
10310
10311 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10312 let sketch = expect_sketch(sketch_object);
10313 assert_eq!(sketch.constraints.len(), 1);
10314
10315 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10316 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10317 panic!("Expected constraint object");
10318 };
10319 let Constraint::Coincident(coincident) = constraint else {
10320 panic!("Expected coincident constraint");
10321 };
10322 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10323 assert_eq!(coincident.segments, remaining_segments);
10324
10325 mock_ctx.close().await;
10326 }
10327
10328 #[tokio::test(flavor = "multi_thread")]
10329 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10330 let initial_source = "\
10331sketch(on = XY) {
10332 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10333 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10334 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10335 equalLength([line1, line2, line3])
10336}
10337";
10338
10339 let program = Program::parse(initial_source).unwrap().0.unwrap();
10340
10341 let mut frontend = FrontendState::new();
10342
10343 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10344 let mock_ctx = ExecutorContext::new_mock(None).await;
10345 let version = Version(0);
10346
10347 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10348 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10349 let sketch_id = sketch_object.id;
10350 let sketch = expect_sketch(sketch_object);
10351 let line2_id = *sketch.segments.get(5).unwrap();
10352 let line3_id = *sketch.segments.get(8).unwrap();
10353
10354 let (src_delta, scene_delta) = frontend
10355 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10356 .await
10357 .unwrap();
10358 insta::assert_snapshot!(
10359 "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10360 src_delta.text.as_str()
10361 );
10362
10363 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10364 let sketch = expect_sketch(sketch_object);
10365 assert!(sketch.constraints.is_empty());
10366
10367 ctx.close().await;
10368 mock_ctx.close().await;
10369 }
10370
10371 #[tokio::test(flavor = "multi_thread")]
10372 async fn test_delete_line_preserves_multiline_parallel_constraint() {
10373 let initial_source = "\
10374sketch(on = XY) {
10375 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10376 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10377 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10378 parallel([line1, line2, line3])
10379}
10380";
10381
10382 let program = Program::parse(initial_source).unwrap().0.unwrap();
10383
10384 let mut frontend = FrontendState::new();
10385
10386 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10387 let mock_ctx = ExecutorContext::new_mock(None).await;
10388 let version = Version(0);
10389
10390 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10391 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10392 let sketch_id = sketch_object.id;
10393 let sketch = expect_sketch(sketch_object);
10394 let line3_id = *sketch.segments.get(8).unwrap();
10395
10396 let (src_delta, scene_delta) = frontend
10397 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10398 .await
10399 .unwrap();
10400 insta::assert_snapshot!(
10401 "test_delete_line_preserves_multiline_parallel_constraint",
10402 src_delta.text.as_str()
10403 );
10404
10405 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10406 let sketch = expect_sketch(sketch_object);
10407 assert_eq!(sketch.constraints.len(), 1);
10408
10409 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10410 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10411 panic!("Expected constraint object");
10412 };
10413 let Constraint::Parallel(parallel) = constraint else {
10414 panic!("Expected parallel constraint");
10415 };
10416 assert_eq!(parallel.lines.len(), 2);
10417
10418 ctx.close().await;
10419 mock_ctx.close().await;
10420 }
10421
10422 #[tokio::test(flavor = "multi_thread")]
10423 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10424 let initial_source = "\
10425sketch(on = XY) {
10426 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10427 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10428 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10429 parallel([line1, line2, line3])
10430}
10431";
10432
10433 let program = Program::parse(initial_source).unwrap().0.unwrap();
10434
10435 let mut frontend = FrontendState::new();
10436
10437 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10438 let mock_ctx = ExecutorContext::new_mock(None).await;
10439 let version = Version(0);
10440
10441 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10442 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10443 let sketch_id = sketch_object.id;
10444 let sketch = expect_sketch(sketch_object);
10445 let line2_id = *sketch.segments.get(5).unwrap();
10446 let line3_id = *sketch.segments.get(8).unwrap();
10447
10448 let (src_delta, scene_delta) = frontend
10449 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10450 .await
10451 .unwrap();
10452 insta::assert_snapshot!(
10453 "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10454 src_delta.text.as_str()
10455 );
10456
10457 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10458 let sketch = expect_sketch(sketch_object);
10459 assert!(sketch.constraints.is_empty());
10460
10461 ctx.close().await;
10462 mock_ctx.close().await;
10463 }
10464
10465 #[tokio::test(flavor = "multi_thread")]
10466 async fn test_delete_line_line_coincident_constraint() {
10467 let initial_source = "\
10468sketch(on = XY) {
10469 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10470 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10471 coincident([line1, line2])
10472}
10473";
10474
10475 let program = Program::parse(initial_source).unwrap().0.unwrap();
10476
10477 let mut frontend = FrontendState::new();
10478
10479 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10480 let mock_ctx = ExecutorContext::new_mock(None).await;
10481 let version = Version(0);
10482
10483 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10484 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10485 let sketch_id = sketch_object.id;
10486 let sketch = expect_sketch(sketch_object);
10487
10488 let coincident_id = *sketch.constraints.first().unwrap();
10489
10490 let (src_delta, scene_delta) = frontend
10491 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10492 .await
10493 .unwrap();
10494 insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10495 assert_eq!(scene_delta.new_objects, vec![]);
10496 assert_eq!(scene_delta.new_graph.objects.len(), 8);
10497
10498 ctx.close().await;
10499 mock_ctx.close().await;
10500 }
10501
10502 #[tokio::test(flavor = "multi_thread")]
10503 async fn test_two_points_coincident() {
10504 let initial_source = "\
10505sketch(on = XY) {
10506 point1 = point(at = [var 1, var 2])
10507 point(at = [3, 4])
10508}
10509";
10510
10511 let program = Program::parse(initial_source).unwrap().0.unwrap();
10512
10513 let mut frontend = FrontendState::new();
10514
10515 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10516 let mock_ctx = ExecutorContext::new_mock(None).await;
10517 let version = Version(0);
10518
10519 frontend.hack_set_program(&ctx, program).await.unwrap();
10520 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10521 let sketch_id = sketch_object.id;
10522 let sketch = expect_sketch(sketch_object);
10523 let point0_id = *sketch.segments.first().unwrap();
10524 let point1_id = *sketch.segments.get(1).unwrap();
10525
10526 let constraint = Constraint::Coincident(Coincident {
10527 segments: vec![point0_id.into(), point1_id.into()],
10528 });
10529 let (src_delta, scene_delta) = frontend
10530 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10531 .await
10532 .unwrap();
10533 insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10534 assert_eq!(
10535 scene_delta.new_graph.objects.len(),
10536 5,
10537 "{:#?}",
10538 scene_delta.new_graph.objects
10539 );
10540
10541 ctx.close().await;
10542 mock_ctx.close().await;
10543 }
10544
10545 #[tokio::test(flavor = "multi_thread")]
10546 async fn test_three_points_coincident() {
10547 let initial_source = "\
10548sketch(on = XY) {
10549 point1 = point(at = [var 1, var 2])
10550 point(at = [var 3, var 4])
10551 point(at = [var 5, var 6])
10552}
10553";
10554
10555 let program = Program::parse(initial_source).unwrap().0.unwrap();
10556
10557 let mut frontend = FrontendState::new();
10558
10559 let mock_ctx = ExecutorContext::new_mock(None).await;
10560 let version = Version(0);
10561
10562 frontend.program = program.clone();
10563 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10564 frontend.update_state_after_exec(outcome, true);
10565 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10566 let sketch_id = sketch_object.id;
10567 let sketch = expect_sketch(sketch_object);
10568 let segments = sketch
10569 .segments
10570 .iter()
10571 .take(3)
10572 .copied()
10573 .map(Into::into)
10574 .collect::<Vec<ConstraintSegment>>();
10575
10576 let constraint = Constraint::Coincident(Coincident {
10577 segments: segments.clone(),
10578 });
10579 let (src_delta, scene_delta) = frontend
10580 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10581 .await
10582 .unwrap();
10583 insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10584
10585 let constraint_object = scene_delta
10586 .new_graph
10587 .objects
10588 .iter()
10589 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10590 .unwrap();
10591
10592 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10593 panic!("expected a constraint object");
10594 };
10595
10596 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10597
10598 mock_ctx.close().await;
10599 }
10600
10601 #[tokio::test(flavor = "multi_thread")]
10602 async fn test_source_with_three_point_coincident_tracks_all_segments() {
10603 let initial_source = "\
10604sketch(on = XY) {
10605 point1 = point(at = [var 1, var 2])
10606 point2 = point(at = [var 3, var 4])
10607 point3 = point(at = [var 5, var 6])
10608 coincident([point1, point2, point3])
10609}
10610";
10611
10612 let program = Program::parse(initial_source).unwrap().0.unwrap();
10613
10614 let mut frontend = FrontendState::new();
10615
10616 let ctx = ExecutorContext::new_mock(None).await;
10617 frontend.program = program.clone();
10618 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10619 frontend.update_state_after_exec(outcome, true);
10620
10621 let constraint_object = frontend
10622 .scene_graph
10623 .objects
10624 .iter()
10625 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10626 .unwrap();
10627 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10628 panic!("expected a constraint object");
10629 };
10630
10631 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10632 let sketch = expect_sketch(sketch_object);
10633 let expected_segments = sketch
10634 .segments
10635 .iter()
10636 .take(3)
10637 .copied()
10638 .map(Into::into)
10639 .collect::<Vec<ConstraintSegment>>();
10640
10641 assert_eq!(
10642 constraint,
10643 &Constraint::Coincident(Coincident {
10644 segments: expected_segments,
10645 })
10646 );
10647
10648 ctx.close().await;
10649 }
10650
10651 #[tokio::test(flavor = "multi_thread")]
10652 async fn test_point_origin_coincident_preserves_order() {
10653 let initial_source = "\
10654sketch(on = XY) {
10655 point(at = [var 1, var 2])
10656}
10657";
10658
10659 for (origin_first, snapshot_name) in [
10660 (true, "test_point_origin_coincident_preserves_order_origin_first"),
10661 (false, "test_point_origin_coincident_preserves_order_point_first"),
10662 ] {
10663 let program = Program::parse(initial_source).unwrap().0.unwrap();
10664
10665 let mut frontend = FrontendState::new();
10666
10667 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10668 let mock_ctx = ExecutorContext::new_mock(None).await;
10669 let version = Version(0);
10670
10671 frontend.hack_set_program(&ctx, program).await.unwrap();
10672 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10673 let sketch_id = sketch_object.id;
10674 let sketch = expect_sketch(sketch_object);
10675 let point_id = *sketch.segments.first().unwrap();
10676
10677 let segments = if origin_first {
10678 vec![ConstraintSegment::ORIGIN, point_id.into()]
10679 } else {
10680 vec![point_id.into(), ConstraintSegment::ORIGIN]
10681 };
10682 let constraint = Constraint::Coincident(Coincident {
10683 segments: segments.clone(),
10684 });
10685 let (src_delta, scene_delta) = frontend
10686 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10687 .await
10688 .unwrap();
10689 insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10690
10691 let constraint_object = scene_delta
10692 .new_graph
10693 .objects
10694 .iter()
10695 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10696 .unwrap();
10697
10698 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10699 panic!("expected a constraint object");
10700 };
10701
10702 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10703
10704 ctx.close().await;
10705 mock_ctx.close().await;
10706 }
10707 }
10708
10709 #[tokio::test(flavor = "multi_thread")]
10710 async fn test_coincident_of_line_end_points() {
10711 let initial_source = "\
10712sketch(on = XY) {
10713 line(start = [var 1, var 2], end = [var 3, var 4])
10714 line(start = [var 5, var 6], end = [var 7, var 8])
10715}
10716";
10717
10718 let program = Program::parse(initial_source).unwrap().0.unwrap();
10719
10720 let mut frontend = FrontendState::new();
10721
10722 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10723 let mock_ctx = ExecutorContext::new_mock(None).await;
10724 let version = Version(0);
10725
10726 frontend.hack_set_program(&ctx, program).await.unwrap();
10727 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10728 let sketch_id = sketch_object.id;
10729 let sketch = expect_sketch(sketch_object);
10730 let point0_id = *sketch.segments.get(1).unwrap();
10731 let point1_id = *sketch.segments.get(3).unwrap();
10732
10733 let constraint = Constraint::Coincident(Coincident {
10734 segments: vec![point0_id.into(), point1_id.into()],
10735 });
10736 let (src_delta, scene_delta) = frontend
10737 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10738 .await
10739 .unwrap();
10740 insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10741 assert_eq!(
10742 scene_delta.new_graph.objects.len(),
10743 9,
10744 "{:#?}",
10745 scene_delta.new_graph.objects
10746 );
10747
10748 ctx.close().await;
10749 mock_ctx.close().await;
10750 }
10751
10752 #[tokio::test(flavor = "multi_thread")]
10753 async fn test_coincident_of_line_point_and_circle_segment() {
10754 let initial_source = "\
10755sketch(on = XY) {
10756 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10757 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10758}
10759";
10760 let program = Program::parse(initial_source).unwrap().0.unwrap();
10761 let mut frontend = FrontendState::new();
10762
10763 let mock_ctx = ExecutorContext::new_mock(None).await;
10764 let version = Version(0);
10765
10766 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10767 frontend.program = program;
10768 frontend.update_state_after_exec(outcome, true);
10769 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10770 let sketch_id = sketch_object.id;
10771 let sketch = expect_sketch(sketch_object);
10772
10773 let circle_id = sketch
10774 .segments
10775 .iter()
10776 .copied()
10777 .find(|seg_id| {
10778 matches!(
10779 &frontend.scene_graph.objects[seg_id.0].kind,
10780 ObjectKind::Segment {
10781 segment: Segment::Circle(_)
10782 }
10783 )
10784 })
10785 .expect("Expected a circle segment in sketch");
10786 let line_id = frontend
10787 .scene_graph
10788 .objects
10789 .iter()
10790 .find_map(|obj| match &obj.kind {
10791 ObjectKind::Segment {
10792 segment: Segment::Line(line),
10793 } if line.owner.is_none() => Some(obj.id),
10794 _ => None,
10795 })
10796 .expect("Expected a standalone line segment in scene graph");
10797
10798 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10799 ObjectKind::Segment {
10800 segment: Segment::Line(line),
10801 } => line.start,
10802 _ => panic!("Expected line segment object"),
10803 };
10804
10805 let constraint = Constraint::Coincident(Coincident {
10806 segments: vec![line_start_point_id.into(), circle_id.into()],
10807 });
10808 let (src_delta, _scene_delta) = frontend
10809 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10810 .await
10811 .unwrap();
10812 insta::assert_snapshot!(
10813 "test_coincident_of_line_point_and_circle_segment",
10814 src_delta.text.as_str()
10815 );
10816
10817 mock_ctx.close().await;
10818 }
10819
10820 #[tokio::test(flavor = "multi_thread")]
10821 async fn test_invalid_coincident_arc_and_line_preserves_state() {
10822 let program = Program::empty();
10830
10831 let mut frontend = FrontendState::new();
10832 frontend.program = program;
10833
10834 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10835 let mock_ctx = ExecutorContext::new_mock(None).await;
10836 let version = Version(0);
10837
10838 let sketch_args = SketchCtor {
10839 on: Plane::Default(PlaneName::Xy),
10840 };
10841 let (_src_delta, _scene_delta, sketch_id) = frontend
10842 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10843 .await
10844 .unwrap();
10845
10846 let arc_ctor = ArcCtor {
10848 start: Point2d {
10849 x: Expr::Var(Number {
10850 value: 0.0,
10851 units: NumericSuffix::Mm,
10852 }),
10853 y: Expr::Var(Number {
10854 value: 0.0,
10855 units: NumericSuffix::Mm,
10856 }),
10857 },
10858 end: Point2d {
10859 x: Expr::Var(Number {
10860 value: 10.0,
10861 units: NumericSuffix::Mm,
10862 }),
10863 y: Expr::Var(Number {
10864 value: 10.0,
10865 units: NumericSuffix::Mm,
10866 }),
10867 },
10868 center: Point2d {
10869 x: Expr::Var(Number {
10870 value: 10.0,
10871 units: NumericSuffix::Mm,
10872 }),
10873 y: Expr::Var(Number {
10874 value: 0.0,
10875 units: NumericSuffix::Mm,
10876 }),
10877 },
10878 direction: None,
10879 construction: None,
10880 };
10881 let (_src_delta, scene_delta) = frontend
10882 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10883 .await
10884 .unwrap();
10885 let arc_id = *scene_delta.new_objects.last().unwrap();
10887
10888 let line_ctor = LineCtor {
10890 start: Point2d {
10891 x: Expr::Var(Number {
10892 value: 20.0,
10893 units: NumericSuffix::Mm,
10894 }),
10895 y: Expr::Var(Number {
10896 value: 0.0,
10897 units: NumericSuffix::Mm,
10898 }),
10899 },
10900 end: Point2d {
10901 x: Expr::Var(Number {
10902 value: 30.0,
10903 units: NumericSuffix::Mm,
10904 }),
10905 y: Expr::Var(Number {
10906 value: 10.0,
10907 units: NumericSuffix::Mm,
10908 }),
10909 },
10910 construction: None,
10911 };
10912 let (_src_delta, scene_delta) = frontend
10913 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10914 .await
10915 .unwrap();
10916 let line_id = *scene_delta.new_objects.last().unwrap();
10918
10919 let constraint = Constraint::Coincident(Coincident {
10922 segments: vec![arc_id.into(), line_id.into()],
10923 });
10924 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10925
10926 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10928
10929 let sketch_object_after =
10932 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10933 let sketch_after = expect_sketch(sketch_object_after);
10934
10935 assert!(
10937 sketch_after.segments.contains(&arc_id),
10938 "Arc segment should still exist after failed constraint"
10939 );
10940 assert!(
10941 sketch_after.segments.contains(&line_id),
10942 "Line segment should still exist after failed constraint"
10943 );
10944
10945 let arc_obj = frontend
10947 .scene_graph
10948 .objects
10949 .get(arc_id.0)
10950 .expect("Arc object should still be accessible");
10951 let line_obj = frontend
10952 .scene_graph
10953 .objects
10954 .get(line_id.0)
10955 .expect("Line object should still be accessible");
10956
10957 match &arc_obj.kind {
10960 ObjectKind::Segment {
10961 segment: Segment::Arc(_),
10962 } => {}
10963 _ => panic!("Arc object should still be an arc segment"),
10964 }
10965 match &line_obj.kind {
10966 ObjectKind::Segment {
10967 segment: Segment::Line(_),
10968 } => {}
10969 _ => panic!("Line object should still be a line segment"),
10970 }
10971
10972 ctx.close().await;
10973 mock_ctx.close().await;
10974 }
10975
10976 #[tokio::test(flavor = "multi_thread")]
10977 async fn test_distance_two_points() {
10978 let initial_source = "\
10979sketch(on = XY) {
10980 point(at = [var 1, var 2])
10981 point(at = [var 3, var 4])
10982}
10983";
10984
10985 let program = Program::parse(initial_source).unwrap().0.unwrap();
10986
10987 let mut frontend = FrontendState::new();
10988
10989 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10990 let mock_ctx = ExecutorContext::new_mock(None).await;
10991 let version = Version(0);
10992
10993 frontend.hack_set_program(&ctx, program).await.unwrap();
10994 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10995 let sketch_id = sketch_object.id;
10996 let sketch = expect_sketch(sketch_object);
10997 let point0_id = *sketch.segments.first().unwrap();
10998 let point1_id = *sketch.segments.get(1).unwrap();
10999
11000 let constraint = Constraint::Distance(Distance {
11001 segments: vec![point0_id.into(), point1_id.into()],
11002 distance: Number {
11003 value: 2.0,
11004 units: NumericSuffix::Mm,
11005 },
11006 label_position: None,
11007 source: Default::default(),
11008 });
11009 let (src_delta, scene_delta) = frontend
11010 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11011 .await
11012 .unwrap();
11013 insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
11014 assert_eq!(
11015 scene_delta.new_graph.objects.len(),
11016 5,
11017 "{:#?}",
11018 scene_delta.new_graph.objects
11019 );
11020
11021 ctx.close().await;
11022 mock_ctx.close().await;
11023 }
11024
11025 #[tokio::test(flavor = "multi_thread")]
11026 async fn test_distance_two_points_with_label() {
11027 let initial_source = "\
11028sketch(on = XY) {
11029 point(at = [var 1, var 2])
11030 point(at = [var 3, var 4])
11031}
11032";
11033
11034 let program = Program::parse(initial_source).unwrap().0.unwrap();
11035
11036 let mut frontend = FrontendState::new();
11037
11038 let mock_ctx = ExecutorContext::new_mock(None).await;
11039 let version = Version(0);
11040
11041 frontend.program = program.clone();
11042 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11043 frontend.update_state_after_exec(outcome, true);
11044 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11045 let sketch_id = sketch_object.id;
11046 let sketch = expect_sketch(sketch_object);
11047 let point0_id = *sketch.segments.first().unwrap();
11048 let point1_id = *sketch.segments.get(1).unwrap();
11049
11050 let label_position = Point2d {
11051 x: Number {
11052 value: 10.0,
11053 units: NumericSuffix::Mm,
11054 },
11055 y: Number {
11056 value: 11.0,
11057 units: NumericSuffix::Mm,
11058 },
11059 };
11060 let constraint = Constraint::Distance(Distance {
11061 segments: vec![point0_id.into(), point1_id.into()],
11062 distance: Number {
11063 value: 2.0,
11064 units: NumericSuffix::Mm,
11065 },
11066 label_position: Some(label_position.clone()),
11067 source: Default::default(),
11068 });
11069 let (src_delta, scene_delta) = frontend
11070 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11071 .await
11072 .unwrap();
11073 insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11074
11075 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11076 let sketch = expect_sketch(sketch_object);
11077 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11078 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11079 panic!("Expected constraint object");
11080 };
11081 let Constraint::Distance(distance) = constraint else {
11082 panic!("Expected distance constraint");
11083 };
11084 assert_eq!(distance.label_position, Some(label_position));
11085
11086 mock_ctx.close().await;
11087 }
11088
11089 #[tokio::test(flavor = "multi_thread")]
11090 async fn test_edit_distance_constraint_label_position() {
11091 let initial_source = "\
11092sketch(on = XY) {
11093 point(at = [var 1, var 2])
11094 point(at = [var 3, var 2])
11095}
11096";
11097
11098 let program = Program::parse(initial_source).unwrap().0.unwrap();
11099
11100 let mut frontend = FrontendState::new();
11101
11102 let mock_ctx = ExecutorContext::new_mock(None).await;
11103 let version = Version(0);
11104
11105 frontend.program = program.clone();
11106 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11107 frontend.update_state_after_exec(outcome, true);
11108 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11109 let sketch_id = sketch_object.id;
11110 let sketch = expect_sketch(sketch_object);
11111 let point0_id = *sketch.segments.first().unwrap();
11112 let point1_id = *sketch.segments.get(1).unwrap();
11113
11114 let constraint = Constraint::Distance(Distance {
11115 segments: vec![point0_id.into(), point1_id.into()],
11116 distance: Number {
11117 value: 2.0,
11118 units: NumericSuffix::Mm,
11119 },
11120 label_position: None,
11121 source: Default::default(),
11122 });
11123 let (_, scene_delta) = frontend
11124 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11125 .await
11126 .unwrap();
11127 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11128 let sketch = expect_sketch(sketch_object);
11129 let constraint_id = sketch.constraints[0];
11130 let label_position = Point2d {
11131 x: Number {
11132 value: 10.0,
11133 units: NumericSuffix::Mm,
11134 },
11135 y: Number {
11136 value: 11.0,
11137 units: NumericSuffix::Mm,
11138 },
11139 };
11140
11141 let (src_delta, scene_delta) = frontend
11142 .edit_distance_constraint_label_position(
11143 &mock_ctx,
11144 version,
11145 sketch_id,
11146 constraint_id,
11147 label_position.clone(),
11148 vec![],
11149 )
11150 .await
11151 .unwrap();
11152 insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11153
11154 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11155 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11156 panic!("Expected constraint object");
11157 };
11158 let Constraint::Distance(distance) = constraint else {
11159 panic!("Expected distance constraint");
11160 };
11161 assert_eq!(distance.label_position, Some(label_position));
11162
11163 mock_ctx.close().await;
11164 }
11165
11166 #[tokio::test(flavor = "multi_thread")]
11167 async fn test_edit_distance_constraint_type_and_value() {
11168 let initial_source = "\
11169sketch(on = XY) {
11170 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11171 distance([line1.start, line1.end]) == 5mm
11172}
11173";
11174
11175 let program = Program::parse(initial_source).unwrap().0.unwrap();
11176 let mut frontend = FrontendState::new();
11177 let mock_ctx = ExecutorContext::new_mock(None).await;
11178 let version = Version(0);
11179
11180 frontend.program = program.clone();
11181 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11182 frontend.update_state_after_exec(outcome, true);
11183 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11184 let sketch_id = sketch_object.id;
11185 let sketch = expect_sketch(sketch_object);
11186 let constraint_id = sketch.constraints[0];
11187 let point0_id = sketch.segments[0];
11188 let point1_id = sketch.segments[1];
11189 let label_position = Point2d {
11190 x: Number {
11191 value: 2.0,
11192 units: NumericSuffix::Mm,
11193 },
11194 y: Number {
11195 value: 5.0,
11196 units: NumericSuffix::Mm,
11197 },
11198 };
11199
11200 let (source_delta, scene_delta) = frontend
11201 .edit_distance_constraint_with_options(
11202 &mock_ctx,
11203 version,
11204 sketch_id,
11205 constraint_id,
11206 Constraint::HorizontalDistance(Distance {
11207 segments: vec![point0_id.into(), point1_id.into()],
11208 distance: Number {
11209 value: 4.0,
11210 units: NumericSuffix::Mm,
11211 },
11212 label_position: Some(label_position.clone()),
11213 source: Default::default(),
11214 }),
11215 EditConstraintOptions {
11216 commit_solved_initial_guesses: false,
11217 },
11218 )
11219 .await
11220 .unwrap();
11221 assert_eq!(
11222 source_delta.text,
11223 "\
11224sketch(on = XY) {
11225 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11226 horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11227}
11228"
11229 );
11230
11231 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11232 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11233 panic!("Expected constraint object");
11234 };
11235 let Constraint::HorizontalDistance(distance) = constraint else {
11236 panic!("Expected horizontal distance constraint");
11237 };
11238 assert_eq!(distance.distance.value, 4.0);
11239 assert_eq!(distance.label_position, Some(label_position));
11240
11241 mock_ctx.close().await;
11242 }
11243
11244 #[tokio::test(flavor = "multi_thread")]
11245 async fn test_edit_angle_constraint_label_position() {
11246 let initial_source = "\
11247sketch(on = XY) {
11248 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11249 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11250 angle([line1, line2]) == 60deg
11251}
11252";
11253
11254 let program = Program::parse(initial_source).unwrap().0.unwrap();
11255 let mut frontend = FrontendState::new();
11256 let mock_ctx = ExecutorContext::new_mock(None).await;
11257 let version = Version(0);
11258
11259 frontend.program = program.clone();
11260 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11261 frontend.update_state_after_exec(outcome, true);
11262 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11263 let sketch_id = sketch_object.id;
11264 let sketch = expect_sketch(sketch_object);
11265 let constraint_id = sketch.constraints[0];
11266 let label_position = Point2d {
11267 x: Number {
11268 value: 10.0,
11269 units: NumericSuffix::Mm,
11270 },
11271 y: Number {
11272 value: 11.0,
11273 units: NumericSuffix::Mm,
11274 },
11275 };
11276
11277 let (src_delta, scene_delta) = frontend
11278 .edit_distance_constraint_label_position(
11279 &mock_ctx,
11280 version,
11281 sketch_id,
11282 constraint_id,
11283 label_position.clone(),
11284 vec![],
11285 )
11286 .await
11287 .unwrap();
11288 assert_eq!(
11289 src_delta.text.as_str(),
11290 "\
11291sketch(on = XY) {
11292 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11293 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11294 angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11295}
11296"
11297 );
11298
11299 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11300 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11301 panic!("Expected constraint object");
11302 };
11303 let Constraint::Angle(angle) = constraint else {
11304 panic!("Expected angle constraint");
11305 };
11306 assert_eq!(angle.label_position, Some(label_position));
11307
11308 mock_ctx.close().await;
11309 }
11310
11311 #[tokio::test(flavor = "multi_thread")]
11312 async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11313 let initial_source = "\
11314sketch(on = XY) {
11315 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11316 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11317 60deg == angleDimension(lines = [line1, line2], sector = 1)
11318}
11319";
11320
11321 let program = Program::parse(initial_source).unwrap().0.unwrap();
11322 let mut frontend = FrontendState::new();
11323 let mock_ctx = ExecutorContext::new_mock(None).await;
11324 let version = Version(0);
11325
11326 frontend.program = program.clone();
11327 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11328 frontend.update_state_after_exec(outcome, true);
11329 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11330 let sketch_id = sketch_object.id;
11331 let sketch = expect_sketch(sketch_object);
11332 let constraint_id = sketch.constraints[0];
11333 let label_position = Point2d {
11334 x: Number {
11335 value: 10.0,
11336 units: NumericSuffix::Mm,
11337 },
11338 y: Number {
11339 value: 11.0,
11340 units: NumericSuffix::Mm,
11341 },
11342 };
11343
11344 let (src_delta, scene_delta) = frontend
11345 .edit_distance_constraint_label_position(
11346 &mock_ctx,
11347 version,
11348 sketch_id,
11349 constraint_id,
11350 label_position.clone(),
11351 vec![],
11352 )
11353 .await
11354 .unwrap();
11355 assert_eq!(
11356 src_delta.text.as_str(),
11357 "\
11358sketch(on = XY) {
11359 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11360 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11361 60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11362}
11363"
11364 );
11365
11366 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11367 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11368 panic!("Expected constraint object");
11369 };
11370 let Constraint::Angle(angle) = constraint else {
11371 panic!("Expected angle constraint");
11372 };
11373 assert_eq!(angle.label_position, Some(label_position));
11374
11375 mock_ctx.close().await;
11376 }
11377
11378 #[tokio::test(flavor = "multi_thread")]
11379 async fn test_edit_angle_constraint() {
11380 let initial_source = "\
11381sketch(on = XY) {
11382 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11383 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11384 angle([line1, line2]) == 60deg
11385}
11386";
11387
11388 let program = Program::parse(initial_source).unwrap().0.unwrap();
11389 let mut frontend = FrontendState::new();
11390 let mock_ctx = ExecutorContext::new_mock(None).await;
11391 let version = Version(0);
11392
11393 frontend.program = program.clone();
11394 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11395 frontend.update_state_after_exec(outcome, true);
11396 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11397 let sketch_id = sketch_object.id;
11398 let sketch = expect_sketch(sketch_object);
11399 let constraint_id = sketch.constraints[0];
11400 let line1_id = *sketch.segments.get(2).unwrap();
11401 let line2_id = *sketch.segments.get(5).unwrap();
11402 let label_position = Point2d {
11403 x: Number {
11404 value: 10.0,
11405 units: NumericSuffix::Mm,
11406 },
11407 y: Number {
11408 value: 11.0,
11409 units: NumericSuffix::Mm,
11410 },
11411 };
11412
11413 let (src_delta, scene_delta) = frontend
11414 .edit_angle_constraint_with_options(
11415 &mock_ctx,
11416 version,
11417 sketch_id,
11418 constraint_id,
11419 Angle {
11420 lines: vec![line2_id, line1_id],
11421 angle: Number {
11422 value: 60.0,
11423 units: NumericSuffix::Deg,
11424 },
11425 sector: Some(3),
11426 inverse: Some(false),
11427 label_position: Some(label_position.clone()),
11428 source: Default::default(),
11429 },
11430 EditConstraintOptions {
11431 commit_solved_initial_guesses: false,
11432 },
11433 )
11434 .await
11435 .unwrap();
11436 assert_eq!(
11437 src_delta.text.as_str(),
11438 "\
11439sketch(on = XY) {
11440 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11441 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11442 angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11443}
11444"
11445 );
11446
11447 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11448 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11449 panic!("Expected constraint object");
11450 };
11451 let Constraint::Angle(angle) = constraint else {
11452 panic!("Expected angle constraint");
11453 };
11454 assert_eq!(angle.lines, vec![line2_id, line1_id]);
11455 assert_eq!(angle.sector, Some(3));
11456 assert_eq!(angle.inverse, Some(false));
11457 assert_eq!(angle.label_position, Some(label_position));
11458
11459 mock_ctx.close().await;
11460 }
11461
11462 #[tokio::test(flavor = "multi_thread")]
11463 async fn test_edit_angle_constraint_with_call_on_right() {
11464 let initial_source = "\
11465sketch(on = XY) {
11466 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11467 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11468 60deg == angle([line1, line2])
11469}
11470";
11471
11472 let program = Program::parse(initial_source).unwrap().0.unwrap();
11473 let mut frontend = FrontendState::new();
11474 let mock_ctx = ExecutorContext::new_mock(None).await;
11475 let version = Version(0);
11476
11477 frontend.program = program.clone();
11478 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11479 frontend.update_state_after_exec(outcome, true);
11480 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11481 let sketch_id = sketch_object.id;
11482 let sketch = expect_sketch(sketch_object);
11483 let constraint_id = sketch.constraints[0];
11484 let line1_id = *sketch.segments.get(2).unwrap();
11485 let line2_id = *sketch.segments.get(5).unwrap();
11486
11487 let (src_delta, _) = frontend
11488 .edit_angle_constraint_with_options(
11489 &mock_ctx,
11490 version,
11491 sketch_id,
11492 constraint_id,
11493 Angle {
11494 lines: vec![line2_id, line1_id],
11495 angle: Number {
11496 value: 60.0,
11497 units: NumericSuffix::Deg,
11498 },
11499 sector: Some(3),
11500 inverse: Some(false),
11501 label_position: None,
11502 source: Default::default(),
11503 },
11504 EditConstraintOptions {
11505 commit_solved_initial_guesses: false,
11506 },
11507 )
11508 .await
11509 .unwrap();
11510 assert_eq!(
11511 src_delta.text.as_str(),
11512 "\
11513sketch(on = XY) {
11514 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11515 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11516 60deg == angleDimension(lines = [line2, line1], sector = 3)
11517}
11518"
11519 );
11520
11521 mock_ctx.close().await;
11522 }
11523
11524 #[tokio::test(flavor = "multi_thread")]
11525 async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11526 let initial_source = "\
11527@settings(kclVersion = 2.0)
11528
11529sketch001 = sketch(on = XZ) {
11530 line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11531 line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11532 distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11533 vertical([line1.start, ORIGIN])
11534}
11535";
11536
11537 let program = Program::parse(initial_source).unwrap().0.unwrap();
11538 let mut frontend = FrontendState::new();
11539 let mock_ctx = ExecutorContext::new_mock(None).await;
11540 let version = Version(0);
11541
11542 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11543 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11544 let sketch_id = sketch_object.id;
11545 let sketch = expect_sketch(sketch_object);
11546 let constraint_id = sketch
11547 .constraints
11548 .iter()
11549 .copied()
11550 .find(|constraint_id| {
11551 matches!(
11552 frontend.scene_graph.objects[constraint_id.0].kind,
11553 ObjectKind::Constraint {
11554 constraint: Constraint::Distance(_)
11555 }
11556 )
11557 })
11558 .unwrap();
11559 let line1_id = sketch
11560 .segments
11561 .iter()
11562 .copied()
11563 .find(|segment_id| {
11564 matches!(
11565 frontend.scene_graph.objects[segment_id.0].kind,
11566 ObjectKind::Segment {
11567 segment: Segment::Line(_)
11568 }
11569 )
11570 })
11571 .unwrap();
11572 let label_position = Point2d {
11573 x: Number {
11574 value: 7.0,
11575 units: NumericSuffix::Mm,
11576 },
11577 y: Number {
11578 value: 9.0,
11579 units: NumericSuffix::Mm,
11580 },
11581 };
11582
11583 let (source_delta, scene_delta) = frontend
11584 .edit_segments_with_options(
11585 &mock_ctx,
11586 version,
11587 sketch_id,
11588 vec![ExistingSegmentCtor {
11589 id: line1_id,
11590 ctor: SegmentCtor::Line(LineCtor {
11591 start: point_expr_mm(2.0, 15.55),
11592 end: point_expr_mm(-4.03, 11.51),
11593 construction: None,
11594 }),
11595 }],
11596 EditSegmentsOptions {
11597 anchor_segment_ids: Some(vec![]),
11598 drag_anchors: vec![SegmentDragAnchor {
11599 segment_id: line1_id,
11600 target: label_position.clone(),
11601 }],
11602 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11603 constraint_id,
11604 label_position: label_position.clone(),
11605 }],
11606 commit_solved_initial_guesses: true,
11607 },
11608 )
11609 .await
11610 .unwrap();
11611
11612 assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11613 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11614 let ObjectKind::Constraint {
11615 constraint: Constraint::Distance(distance),
11616 } = &constraint_object.kind
11617 else {
11618 panic!("Expected distance constraint object");
11619 };
11620 assert_eq!(distance.label_position, Some(label_position));
11621
11622 let snapped_label_position = Point2d {
11623 x: Number {
11624 value: 8.0,
11625 units: NumericSuffix::Mm,
11626 },
11627 y: Number {
11628 value: 10.0,
11629 units: NumericSuffix::Mm,
11630 },
11631 };
11632 let (source_delta, scene_delta) = frontend
11633 .edit_segments_with_options(
11634 &mock_ctx,
11635 version,
11636 sketch_id,
11637 vec![],
11638 EditSegmentsOptions {
11639 anchor_segment_ids: Some(vec![line1_id]),
11640 drag_anchors: vec![],
11641 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11642 constraint_id,
11643 label_position: snapped_label_position.clone(),
11644 }],
11645 commit_solved_initial_guesses: true,
11646 },
11647 )
11648 .await
11649 .unwrap();
11650
11651 assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11652 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11653 let ObjectKind::Constraint {
11654 constraint: Constraint::Distance(distance),
11655 } = &constraint_object.kind
11656 else {
11657 panic!("Expected distance constraint object");
11658 };
11659 assert_eq!(distance.label_position, Some(snapped_label_position));
11660
11661 mock_ctx.close().await;
11662 }
11663
11664 #[tokio::test(flavor = "multi_thread")]
11665 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11666 let initial_source = "\
11667sketch(on = XY) {
11668 point1 = point(at = [var 0mm, var 0mm])
11669 point2 = point(at = [var 10mm, var 0mm])
11670 distance([point1, point2]) == 5mm
11671}
11672";
11673
11674 let program = Program::parse(initial_source).unwrap().0.unwrap();
11675 let mut frontend = FrontendState::new();
11676 let mock_ctx = ExecutorContext::new_mock(None).await;
11677 let version = Version(0);
11678
11679 frontend.program = program.clone();
11680 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11681 frontend.update_state_after_exec(outcome, true);
11682 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11683 let sketch_id = sketch_object.id;
11684 let sketch = expect_sketch(sketch_object);
11685 let point0_id = sketch.segments[0];
11686 let point1_id = sketch.segments[1];
11687 let constraint_id = sketch.constraints[0];
11688
11689 let edited_segments = vec![ExistingSegmentCtor {
11690 id: point0_id,
11691 ctor: SegmentCtor::Point(PointCtor {
11692 position: Point2d {
11693 x: Expr::Var(Number {
11694 value: 2.0,
11695 units: NumericSuffix::Mm,
11696 }),
11697 y: Expr::Var(Number {
11698 value: 1.0,
11699 units: NumericSuffix::Mm,
11700 }),
11701 },
11702 }),
11703 }];
11704 let (_, scene_delta) = frontend
11705 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11706 .await
11707 .unwrap();
11708 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11709 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11710
11711 let label_position = Point2d {
11712 x: Number {
11713 value: 3.0,
11714 units: NumericSuffix::Mm,
11715 },
11716 y: Number {
11717 value: 4.0,
11718 units: NumericSuffix::Mm,
11719 },
11720 };
11721 let (_, scene_delta) = frontend
11722 .edit_distance_constraint_label_position(
11723 &mock_ctx,
11724 version,
11725 sketch_id,
11726 constraint_id,
11727 label_position,
11728 vec![point0_id],
11729 )
11730 .await
11731 .unwrap();
11732
11733 assert_point_position_close(
11734 point_position(&scene_delta.new_graph, point0_id),
11735 point0_after_segment_edit,
11736 );
11737 assert_point_position_close(
11738 point_position(&scene_delta.new_graph, point1_id),
11739 point1_after_segment_edit,
11740 );
11741
11742 mock_ctx.close().await;
11743 }
11744
11745 #[tokio::test(flavor = "multi_thread")]
11746 async fn test_distance_point_line() {
11747 let initial_source = "\
11748sketch(on = XY) {
11749 point(at = [var 0, var 5])
11750 line(start = [var 0, var 0], end = [var 10, var 0])
11751}
11752";
11753
11754 let program = Program::parse(initial_source).unwrap().0.unwrap();
11755
11756 let mut frontend = FrontendState::new();
11757
11758 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11759 let mock_ctx = ExecutorContext::new_mock(None).await;
11760 let version = Version(0);
11761
11762 frontend.hack_set_program(&ctx, program).await.unwrap();
11763 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11764 let sketch_id = sketch_object.id;
11765 let sketch = expect_sketch(sketch_object);
11766 let point_id = *sketch.segments.first().unwrap();
11767 let line_id = *sketch
11768 .segments
11769 .iter()
11770 .find(|segment_id| {
11771 matches!(
11772 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11773 Some(ObjectKind::Segment {
11774 segment: Segment::Line(_)
11775 })
11776 )
11777 })
11778 .unwrap();
11779
11780 let label_position = Point2d {
11781 x: Number {
11782 value: 10.0,
11783 units: NumericSuffix::Mm,
11784 },
11785 y: Number {
11786 value: 11.0,
11787 units: NumericSuffix::Mm,
11788 },
11789 };
11790 let constraint = Constraint::Distance(Distance {
11791 segments: vec![point_id.into(), line_id.into()],
11792 distance: Number {
11793 value: 5.0,
11794 units: NumericSuffix::Mm,
11795 },
11796 label_position: Some(label_position.clone()),
11797 source: Default::default(),
11798 });
11799 let (src_delta, scene_delta) = frontend
11800 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11801 .await
11802 .unwrap();
11803 insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11804 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11805 let sketch = expect_sketch(sketch_object);
11806 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11807 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11808 panic!("Expected constraint object");
11809 };
11810 let Constraint::Distance(distance) = constraint else {
11811 panic!("Expected distance constraint");
11812 };
11813 assert_eq!(distance.label_position, Some(label_position));
11814
11815 ctx.close().await;
11816 mock_ctx.close().await;
11817 }
11818
11819 #[tokio::test(flavor = "multi_thread")]
11820 async fn test_distance_point_arc() {
11821 let initial_source = "\
11822sketch(on = XY) {
11823 point(at = [var 0, var 8])
11824 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11825}
11826";
11827
11828 let program = Program::parse(initial_source).unwrap().0.unwrap();
11829
11830 let mut frontend = FrontendState::new();
11831
11832 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11833 let mock_ctx = ExecutorContext::new_mock(None).await;
11834 let version = Version(0);
11835
11836 frontend.hack_set_program(&ctx, program).await.unwrap();
11837 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11838 let sketch_id = sketch_object.id;
11839 let sketch = expect_sketch(sketch_object);
11840 let point_id = *sketch.segments.first().unwrap();
11841 let arc_id = *sketch
11842 .segments
11843 .iter()
11844 .find(|segment_id| {
11845 matches!(
11846 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11847 Some(ObjectKind::Segment {
11848 segment: Segment::Arc(_)
11849 })
11850 )
11851 })
11852 .unwrap();
11853
11854 let constraint = Constraint::Distance(Distance {
11855 segments: vec![point_id.into(), arc_id.into()],
11856 distance: Number {
11857 value: 3.0,
11858 units: NumericSuffix::Mm,
11859 },
11860 label_position: None,
11861 source: Default::default(),
11862 });
11863 let (src_delta, _scene_delta) = frontend
11864 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11865 .await
11866 .unwrap();
11867 insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11868
11869 ctx.close().await;
11870 mock_ctx.close().await;
11871 }
11872
11873 #[tokio::test(flavor = "multi_thread")]
11874 async fn test_distance_arc_origin() {
11875 let initial_source = "\
11876sketch001 = sketch(on = XY) {
11877 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11878}
11879";
11880
11881 let program = Program::parse(initial_source).unwrap().0.unwrap();
11882
11883 let mut frontend = FrontendState::new();
11884
11885 let mock_ctx = ExecutorContext::new_mock(None).await;
11886 let version = Version(0);
11887
11888 frontend.program = program.clone();
11889 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11890 frontend.update_state_after_exec(outcome, true);
11891 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11892 let sketch_id = sketch_object.id;
11893 let sketch = expect_sketch(sketch_object);
11894 let arc_id = *sketch
11895 .segments
11896 .iter()
11897 .find(|segment_id| {
11898 matches!(
11899 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11900 Some(ObjectKind::Segment {
11901 segment: Segment::Arc(_)
11902 })
11903 )
11904 })
11905 .unwrap();
11906
11907 let constraint = Constraint::Distance(Distance {
11908 segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11909 distance: Number {
11910 value: 3.0,
11911 units: NumericSuffix::Mm,
11912 },
11913 label_position: None,
11914 source: Default::default(),
11915 });
11916 let (src_delta, _scene_delta) = frontend
11917 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11918 .await
11919 .unwrap();
11920 insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11921
11922 mock_ctx.close().await;
11923 }
11924
11925 #[tokio::test(flavor = "multi_thread")]
11926 async fn test_distance_line_origin() {
11927 let initial_source = "\
11928sketch(on = XY) {
11929 line(start = [var 5, var 0], end = [var 5, var 10])
11930}
11931";
11932
11933 let program = Program::parse(initial_source).unwrap().0.unwrap();
11934
11935 let mut frontend = FrontendState::new();
11936
11937 let mock_ctx = ExecutorContext::new_mock(None).await;
11938 let version = Version(0);
11939
11940 frontend.program = program.clone();
11941 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11942 frontend.update_state_after_exec(outcome, true);
11943 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11944 let sketch_id = sketch_object.id;
11945 let sketch = expect_sketch(sketch_object);
11946 let line_id = *sketch
11947 .segments
11948 .iter()
11949 .find(|segment_id| {
11950 matches!(
11951 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11952 Some(ObjectKind::Segment {
11953 segment: Segment::Line(_)
11954 })
11955 )
11956 })
11957 .unwrap();
11958
11959 let constraint = Constraint::Distance(Distance {
11960 segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11961 distance: Number {
11962 value: 5.0,
11963 units: NumericSuffix::Mm,
11964 },
11965 label_position: None,
11966 source: Default::default(),
11967 });
11968 let (src_delta, _scene_delta) = frontend
11969 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11970 .await
11971 .unwrap();
11972 insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11973
11974 mock_ctx.close().await;
11975 }
11976
11977 #[tokio::test(flavor = "multi_thread")]
11978 async fn test_distance_line_circle() {
11979 let initial_source = "\
11980sketch(on = XY) {
11981 line(start = [var -10, var 8], end = [var 10, var 8])
11982 circle(start = [var 5, var 0], center = [var 0, var 0])
11983}
11984";
11985
11986 let program = Program::parse(initial_source).unwrap().0.unwrap();
11987
11988 let mut frontend = FrontendState::new();
11989
11990 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11991 let mock_ctx = ExecutorContext::new_mock(None).await;
11992 let version = Version(0);
11993
11994 frontend.hack_set_program(&ctx, program).await.unwrap();
11995 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11996 let sketch_id = sketch_object.id;
11997 let sketch = expect_sketch(sketch_object);
11998 let line_id = *sketch
11999 .segments
12000 .iter()
12001 .find(|segment_id| {
12002 matches!(
12003 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12004 Some(ObjectKind::Segment {
12005 segment: Segment::Line(_)
12006 })
12007 )
12008 })
12009 .unwrap();
12010 let circle_id = *sketch
12011 .segments
12012 .iter()
12013 .find(|segment_id| {
12014 matches!(
12015 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12016 Some(ObjectKind::Segment {
12017 segment: Segment::Circle(_)
12018 })
12019 )
12020 })
12021 .unwrap();
12022
12023 let constraint = Constraint::Distance(Distance {
12024 segments: vec![line_id.into(), circle_id.into()],
12025 distance: Number {
12026 value: 3.0,
12027 units: NumericSuffix::Mm,
12028 },
12029 label_position: None,
12030 source: Default::default(),
12031 });
12032 let (src_delta, _scene_delta) = frontend
12033 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12034 .await
12035 .unwrap();
12036 insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
12037
12038 ctx.close().await;
12039 mock_ctx.close().await;
12040 }
12041
12042 #[tokio::test(flavor = "multi_thread")]
12043 async fn test_distance_circle_arc() {
12044 let initial_source = "\
12045sketch(on = XY) {
12046 circle(start = [var 5, var 0], center = [var 0, var 0])
12047 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
12048}
12049";
12050
12051 let program = Program::parse(initial_source).unwrap().0.unwrap();
12052
12053 let mut frontend = FrontendState::new();
12054
12055 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12056 let mock_ctx = ExecutorContext::new_mock(None).await;
12057 let version = Version(0);
12058
12059 frontend.hack_set_program(&ctx, program).await.unwrap();
12060 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12061 let sketch_id = sketch_object.id;
12062 let sketch = expect_sketch(sketch_object);
12063 let circle_id = *sketch
12064 .segments
12065 .iter()
12066 .find(|segment_id| {
12067 matches!(
12068 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12069 Some(ObjectKind::Segment {
12070 segment: Segment::Circle(_)
12071 })
12072 )
12073 })
12074 .unwrap();
12075 let arc_id = *sketch
12076 .segments
12077 .iter()
12078 .find(|segment_id| {
12079 matches!(
12080 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12081 Some(ObjectKind::Segment {
12082 segment: Segment::Arc(_)
12083 })
12084 )
12085 })
12086 .unwrap();
12087
12088 let constraint = Constraint::Distance(Distance {
12089 segments: vec![circle_id.into(), arc_id.into()],
12090 distance: Number {
12091 value: 3.0,
12092 units: NumericSuffix::Mm,
12093 },
12094 label_position: None,
12095 source: Default::default(),
12096 });
12097 let (src_delta, _scene_delta) = frontend
12098 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12099 .await
12100 .unwrap();
12101 insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12102
12103 ctx.close().await;
12104 mock_ctx.close().await;
12105 }
12106
12107 #[tokio::test(flavor = "multi_thread")]
12108 async fn test_distance_parallel_lines() {
12109 let initial_source = "\
12110sketch(on = XY) {
12111 line(start = [var 0, var 0], end = [var 10, var 0])
12112 line(start = [var 0, var 5], end = [var 10, var 5])
12113}
12114";
12115
12116 let program = Program::parse(initial_source).unwrap().0.unwrap();
12117
12118 let mut frontend = FrontendState::new();
12119
12120 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12121 let mock_ctx = ExecutorContext::new_mock(None).await;
12122 let version = Version(0);
12123
12124 frontend.hack_set_program(&ctx, program).await.unwrap();
12125 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12126 let sketch_id = sketch_object.id;
12127 let sketch = expect_sketch(sketch_object);
12128 let line_ids = sketch
12129 .segments
12130 .iter()
12131 .copied()
12132 .filter(|segment_id| {
12133 matches!(
12134 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12135 Some(ObjectKind::Segment {
12136 segment: Segment::Line(_)
12137 })
12138 )
12139 })
12140 .collect::<Vec<_>>();
12141
12142 let constraint = Constraint::Distance(Distance {
12143 segments: vec![line_ids[0].into(), line_ids[1].into()],
12144 distance: Number {
12145 value: 5.0,
12146 units: NumericSuffix::Mm,
12147 },
12148 label_position: None,
12149 source: Default::default(),
12150 });
12151 let (src_delta, _scene_delta) = frontend
12152 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12153 .await
12154 .unwrap();
12155 insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12156
12157 ctx.close().await;
12158 mock_ctx.close().await;
12159 }
12160
12161 #[tokio::test(flavor = "multi_thread")]
12162 async fn test_distance_non_parallel_lines_lowers_to_distance() {
12163 let initial_source = "\
12168sketch(on = XY) {
12169 line(start = [var 0, var 0], end = [var 10, var 0])
12170 line(start = [var 0, var 0], end = [var 10, var 10])
12171}
12172";
12173
12174 let program = Program::parse(initial_source).unwrap().0.unwrap();
12175
12176 let mut frontend = FrontendState::new();
12177
12178 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12179 let mock_ctx = ExecutorContext::new_mock(None).await;
12180 let version = Version(0);
12181
12182 frontend.hack_set_program(&ctx, program).await.unwrap();
12183 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12184 let sketch_id = sketch_object.id;
12185 let sketch = expect_sketch(sketch_object);
12186 let line_ids = sketch
12187 .segments
12188 .iter()
12189 .copied()
12190 .filter(|segment_id| {
12191 matches!(
12192 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12193 Some(ObjectKind::Segment {
12194 segment: Segment::Line(_)
12195 })
12196 )
12197 })
12198 .collect::<Vec<_>>();
12199
12200 let constraint = Constraint::Distance(Distance {
12201 segments: vec![line_ids[0].into(), line_ids[1].into()],
12202 distance: Number {
12203 value: 5.0,
12204 units: NumericSuffix::Mm,
12205 },
12206 label_position: None,
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!(
12214 "test_distance_non_parallel_lines_lowers_to_distance",
12215 src_delta.text.as_str()
12216 );
12217
12218 ctx.close().await;
12219 mock_ctx.close().await;
12220 }
12221
12222 #[tokio::test(flavor = "multi_thread")]
12223 async fn test_horizontal_distance_two_points() {
12224 let initial_source = "\
12225sketch(on = XY) {
12226 point(at = [var 1, var 2])
12227 point(at = [var 3, var 4])
12228}
12229";
12230
12231 let program = Program::parse(initial_source).unwrap().0.unwrap();
12232
12233 let mut frontend = FrontendState::new();
12234
12235 let mock_ctx = ExecutorContext::new_mock(None).await;
12236 let version = Version(0);
12237
12238 frontend.program = program.clone();
12239 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12240 frontend.update_state_after_exec(outcome, true);
12241 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12242 let sketch_id = sketch_object.id;
12243 let sketch = expect_sketch(sketch_object);
12244 let point0_id = *sketch.segments.first().unwrap();
12245 let point1_id = *sketch.segments.get(1).unwrap();
12246 let label_position = Point2d {
12247 x: Number {
12248 value: 10.0,
12249 units: NumericSuffix::Mm,
12250 },
12251 y: Number {
12252 value: 11.0,
12253 units: NumericSuffix::Mm,
12254 },
12255 };
12256
12257 let constraint = Constraint::HorizontalDistance(Distance {
12258 segments: vec![point0_id.into(), point1_id.into()],
12259 distance: Number {
12260 value: 2.0,
12261 units: NumericSuffix::Mm,
12262 },
12263 label_position: Some(label_position.clone()),
12264 source: Default::default(),
12265 });
12266 let (src_delta, scene_delta) = frontend
12267 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12268 .await
12269 .unwrap();
12270 insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12271 assert_eq!(
12272 scene_delta.new_graph.objects.len(),
12273 5,
12274 "{:#?}",
12275 scene_delta.new_graph.objects
12276 );
12277 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12278 let sketch = expect_sketch(sketch_object);
12279 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12280 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12281 panic!("Expected constraint object");
12282 };
12283 let Constraint::HorizontalDistance(distance) = constraint else {
12284 panic!("Expected horizontal distance constraint");
12285 };
12286 assert_eq!(distance.label_position, Some(label_position));
12287
12288 mock_ctx.close().await;
12289 }
12290
12291 #[tokio::test(flavor = "multi_thread")]
12292 async fn test_radius_single_arc_segment() {
12293 let initial_source = "\
12294sketch(on = XY) {
12295 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12296}
12297";
12298
12299 let program = Program::parse(initial_source).unwrap().0.unwrap();
12300
12301 let mut frontend = FrontendState::new();
12302
12303 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12304 let mock_ctx = ExecutorContext::new_mock(None).await;
12305 let version = Version(0);
12306
12307 frontend.hack_set_program(&ctx, program).await.unwrap();
12308 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12309 let sketch_id = sketch_object.id;
12310 let sketch = expect_sketch(sketch_object);
12311 let arc_id = sketch
12313 .segments
12314 .iter()
12315 .find(|&seg_id| {
12316 let obj = frontend.scene_graph.objects.get(seg_id.0);
12317 matches!(
12318 obj.map(|o| &o.kind),
12319 Some(ObjectKind::Segment {
12320 segment: Segment::Arc(_)
12321 })
12322 )
12323 })
12324 .unwrap();
12325
12326 let constraint = Constraint::Radius(Radius {
12327 arc: *arc_id,
12328 radius: Number {
12329 value: 5.0,
12330 units: NumericSuffix::Mm,
12331 },
12332 label_position: None,
12333 source: Default::default(),
12334 });
12335 let (src_delta, scene_delta) = frontend
12336 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12337 .await
12338 .unwrap();
12339 insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12340 assert_eq!(
12341 scene_delta.new_graph.objects.len(),
12342 7, "{:#?}",
12344 scene_delta.new_graph.objects
12345 );
12346
12347 ctx.close().await;
12348 mock_ctx.close().await;
12349 }
12350
12351 #[tokio::test(flavor = "multi_thread")]
12352 async fn test_radius_single_arc_segment_with_label_position() {
12353 let initial_source = "\
12354sketch(on = XY) {
12355 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12356}
12357";
12358
12359 let program = Program::parse(initial_source).unwrap().0.unwrap();
12360 let mut frontend = FrontendState::new();
12361 let mock_ctx = ExecutorContext::new_mock(None).await;
12362 let version = Version(0);
12363
12364 frontend.program = program.clone();
12365 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12366 frontend.update_state_after_exec(outcome, true);
12367 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12368 let sketch_id = sketch_object.id;
12369 let sketch = expect_sketch(sketch_object);
12370 let arc_id = sketch
12371 .segments
12372 .iter()
12373 .find(|&seg_id| {
12374 let obj = frontend.scene_graph.objects.get(seg_id.0);
12375 matches!(
12376 obj.map(|o| &o.kind),
12377 Some(ObjectKind::Segment {
12378 segment: Segment::Arc(_)
12379 })
12380 )
12381 })
12382 .unwrap();
12383
12384 let label_position = Point2d {
12385 x: Number {
12386 value: 10.0,
12387 units: NumericSuffix::Mm,
12388 },
12389 y: Number {
12390 value: 11.0,
12391 units: NumericSuffix::Mm,
12392 },
12393 };
12394 let constraint = Constraint::Radius(Radius {
12395 arc: *arc_id,
12396 radius: Number {
12397 value: 5.0,
12398 units: NumericSuffix::Mm,
12399 },
12400 label_position: Some(label_position.clone()),
12401 source: Default::default(),
12402 });
12403 let (src_delta, scene_delta) = frontend
12404 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12405 .await
12406 .unwrap();
12407 insta::assert_snapshot!(
12408 "test_radius_single_arc_segment_with_label_position",
12409 src_delta.text.as_str()
12410 );
12411
12412 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12413 let sketch = expect_sketch(sketch_object);
12414 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].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_edit_radius_constraint_label_position() {
12428 let initial_source = "\
12429sketch(on = XY) {
12430 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12431 radius(arc1) == 5mm
12432}
12433";
12434
12435 let program = Program::parse(initial_source).unwrap().0.unwrap();
12436 let mut frontend = FrontendState::new();
12437 let mock_ctx = ExecutorContext::new_mock(None).await;
12438 let version = Version(0);
12439
12440 frontend.program = program.clone();
12441 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12442 frontend.update_state_after_exec(outcome, true);
12443 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12444 let sketch_id = sketch_object.id;
12445 let sketch = expect_sketch(sketch_object);
12446 let constraint_id = sketch.constraints[0];
12447 let label_position = Point2d {
12448 x: Number {
12449 value: 10.0,
12450 units: NumericSuffix::Mm,
12451 },
12452 y: Number {
12453 value: 11.0,
12454 units: NumericSuffix::Mm,
12455 },
12456 };
12457
12458 let (src_delta, scene_delta) = frontend
12459 .edit_distance_constraint_label_position(
12460 &mock_ctx,
12461 version,
12462 sketch_id,
12463 constraint_id,
12464 label_position.clone(),
12465 vec![],
12466 )
12467 .await
12468 .unwrap();
12469 insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12470
12471 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12472 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12473 panic!("Expected constraint object");
12474 };
12475 let Constraint::Radius(radius) = constraint else {
12476 panic!("Expected radius constraint");
12477 };
12478 assert_eq!(radius.label_position, Some(label_position));
12479
12480 mock_ctx.close().await;
12481 }
12482
12483 #[tokio::test(flavor = "multi_thread")]
12484 async fn test_vertical_distance_two_points() {
12485 let initial_source = "\
12486sketch(on = XY) {
12487 point(at = [var 1, var 2])
12488 point(at = [var 3, var 4])
12489}
12490";
12491
12492 let program = Program::parse(initial_source).unwrap().0.unwrap();
12493
12494 let mut frontend = FrontendState::new();
12495
12496 let mock_ctx = ExecutorContext::new_mock(None).await;
12497 let version = Version(0);
12498
12499 frontend.program = program.clone();
12500 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12501 frontend.update_state_after_exec(outcome, true);
12502 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12503 let sketch_id = sketch_object.id;
12504 let sketch = expect_sketch(sketch_object);
12505 let point0_id = *sketch.segments.first().unwrap();
12506 let point1_id = *sketch.segments.get(1).unwrap();
12507 let label_position = Point2d {
12508 x: Number {
12509 value: 10.0,
12510 units: NumericSuffix::Mm,
12511 },
12512 y: Number {
12513 value: 11.0,
12514 units: NumericSuffix::Mm,
12515 },
12516 };
12517
12518 let constraint = Constraint::VerticalDistance(Distance {
12519 segments: vec![point0_id.into(), point1_id.into()],
12520 distance: Number {
12521 value: 2.0,
12522 units: NumericSuffix::Mm,
12523 },
12524 label_position: Some(label_position.clone()),
12525 source: Default::default(),
12526 });
12527 let (src_delta, scene_delta) = frontend
12528 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12529 .await
12530 .unwrap();
12531 insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12532 assert_eq!(
12533 scene_delta.new_graph.objects.len(),
12534 5,
12535 "{:#?}",
12536 scene_delta.new_graph.objects
12537 );
12538 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12539 let sketch = expect_sketch(sketch_object);
12540 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12541 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12542 panic!("Expected constraint object");
12543 };
12544 let Constraint::VerticalDistance(distance) = constraint else {
12545 panic!("Expected vertical distance constraint");
12546 };
12547 assert_eq!(distance.label_position, Some(label_position));
12548
12549 mock_ctx.close().await;
12550 }
12551
12552 #[tokio::test(flavor = "multi_thread")]
12553 async fn test_add_fixed_standalone_point() {
12554 let initial_source = "\
12555sketch(on = XY) {
12556 point(at = [var 1, var 2])
12557}
12558";
12559
12560 let program = Program::parse(initial_source).unwrap().0.unwrap();
12561
12562 let mut frontend = FrontendState::new();
12563
12564 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12565 let mock_ctx = ExecutorContext::new_mock(None).await;
12566 let version = Version(0);
12567
12568 frontend.hack_set_program(&ctx, program).await.unwrap();
12569 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12570 let sketch_id = sketch_object.id;
12571 let sketch = expect_sketch(sketch_object);
12572 let point_id = *sketch.segments.first().unwrap();
12573
12574 let (src_delta, scene_delta) = frontend
12575 .add_constraint(
12576 &mock_ctx,
12577 version,
12578 sketch_id,
12579 Constraint::Fixed(Fixed {
12580 points: vec![FixedPoint {
12581 point: point_id,
12582 position: Point2d {
12583 x: Number {
12584 value: 2.0,
12585 units: NumericSuffix::Mm,
12586 },
12587 y: Number {
12588 value: 3.0,
12589 units: NumericSuffix::Mm,
12590 },
12591 },
12592 }],
12593 }),
12594 )
12595 .await
12596 .unwrap();
12597 insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12598 assert_eq!(
12599 scene_delta.new_graph.objects.len(),
12600 4,
12601 "{:#?}",
12602 scene_delta.new_graph.objects
12603 );
12604
12605 ctx.close().await;
12606 mock_ctx.close().await;
12607 }
12608
12609 #[tokio::test(flavor = "multi_thread")]
12610 async fn test_add_fixed_multiple_points() {
12611 let initial_source = "\
12612sketch(on = XY) {
12613 point(at = [var 1, var 2])
12614 point(at = [var 3, var 4])
12615}
12616";
12617
12618 let program = Program::parse(initial_source).unwrap().0.unwrap();
12619
12620 let mut frontend = FrontendState::new();
12621
12622 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12623 let mock_ctx = ExecutorContext::new_mock(None).await;
12624 let version = Version(0);
12625
12626 frontend.hack_set_program(&ctx, program).await.unwrap();
12627 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12628 let sketch_id = sketch_object.id;
12629 let sketch = expect_sketch(sketch_object);
12630 let point0_id = *sketch.segments.first().unwrap();
12631 let point1_id = *sketch.segments.get(1).unwrap();
12632
12633 let (src_delta, scene_delta) = frontend
12634 .add_constraint(
12635 &mock_ctx,
12636 version,
12637 sketch_id,
12638 Constraint::Fixed(Fixed {
12639 points: vec![
12640 FixedPoint {
12641 point: point0_id,
12642 position: Point2d {
12643 x: Number {
12644 value: 2.0,
12645 units: NumericSuffix::Mm,
12646 },
12647 y: Number {
12648 value: 3.0,
12649 units: NumericSuffix::Mm,
12650 },
12651 },
12652 },
12653 FixedPoint {
12654 point: point1_id,
12655 position: Point2d {
12656 x: Number {
12657 value: 4.0,
12658 units: NumericSuffix::Mm,
12659 },
12660 y: Number {
12661 value: 5.0,
12662 units: NumericSuffix::Mm,
12663 },
12664 },
12665 },
12666 ],
12667 }),
12668 )
12669 .await
12670 .unwrap();
12671 insta::assert_snapshot!("test_add_fixed_multiple_points", 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_add_fixed_owned_point() {
12685 let initial_source = "\
12686sketch(on = XY) {
12687 line(start = [var 1, var 2], end = [var 3, var 4])
12688}
12689";
12690
12691 let program = Program::parse(initial_source).unwrap().0.unwrap();
12692
12693 let mut frontend = FrontendState::new();
12694
12695 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12696 let mock_ctx = ExecutorContext::new_mock(None).await;
12697 let version = Version(0);
12698
12699 frontend.hack_set_program(&ctx, program).await.unwrap();
12700 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12701 let sketch_id = sketch_object.id;
12702 let sketch = expect_sketch(sketch_object);
12703 let line_start_id = *sketch.segments.first().unwrap();
12704
12705 let (src_delta, scene_delta) = frontend
12706 .add_constraint(
12707 &mock_ctx,
12708 version,
12709 sketch_id,
12710 Constraint::Fixed(Fixed {
12711 points: vec![FixedPoint {
12712 point: line_start_id,
12713 position: Point2d {
12714 x: Number {
12715 value: 2.0,
12716 units: NumericSuffix::Mm,
12717 },
12718 y: Number {
12719 value: 3.0,
12720 units: NumericSuffix::Mm,
12721 },
12722 },
12723 }],
12724 }),
12725 )
12726 .await
12727 .unwrap();
12728 insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12729 assert_eq!(
12730 scene_delta.new_graph.objects.len(),
12731 6,
12732 "{:#?}",
12733 scene_delta.new_graph.objects
12734 );
12735
12736 ctx.close().await;
12737 mock_ctx.close().await;
12738 }
12739
12740 #[tokio::test(flavor = "multi_thread")]
12741 async fn test_radius_error_cases() {
12742 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12743 let mock_ctx = ExecutorContext::new_mock(None).await;
12744 let version = Version(0);
12745
12746 let initial_source_point = "\
12748sketch(on = XY) {
12749 point(at = [var 1, var 2])
12750}
12751";
12752 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12753 let mut frontend_point = FrontendState::new();
12754 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12755 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12756 let sketch_id_point = sketch_object_point.id;
12757 let sketch_point = expect_sketch(sketch_object_point);
12758 let point_id = *sketch_point.segments.first().unwrap();
12759
12760 let constraint_point = Constraint::Radius(Radius {
12761 arc: point_id,
12762 radius: Number {
12763 value: 5.0,
12764 units: NumericSuffix::Mm,
12765 },
12766 label_position: None,
12767 source: Default::default(),
12768 });
12769 let result_point = frontend_point
12770 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12771 .await;
12772 assert!(result_point.is_err(), "Single point should error for radius");
12773
12774 let initial_source_line = "\
12776sketch(on = XY) {
12777 line(start = [var 1, var 2], end = [var 3, var 4])
12778}
12779";
12780 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12781 let mut frontend_line = FrontendState::new();
12782 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12783 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12784 let sketch_id_line = sketch_object_line.id;
12785 let sketch_line = expect_sketch(sketch_object_line);
12786 let line_id = *sketch_line.segments.first().unwrap();
12787
12788 let constraint_line = Constraint::Radius(Radius {
12789 arc: line_id,
12790 radius: Number {
12791 value: 5.0,
12792 units: NumericSuffix::Mm,
12793 },
12794 label_position: None,
12795 source: Default::default(),
12796 });
12797 let result_line = frontend_line
12798 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12799 .await;
12800 assert!(result_line.is_err(), "Single line segment should error for radius");
12801
12802 ctx.close().await;
12803 mock_ctx.close().await;
12804 }
12805
12806 #[tokio::test(flavor = "multi_thread")]
12807 async fn test_diameter_single_arc_segment() {
12808 let initial_source = "\
12809sketch(on = XY) {
12810 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12811}
12812";
12813
12814 let program = Program::parse(initial_source).unwrap().0.unwrap();
12815
12816 let mut frontend = FrontendState::new();
12817
12818 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12819 let mock_ctx = ExecutorContext::new_mock(None).await;
12820 let version = Version(0);
12821
12822 frontend.hack_set_program(&ctx, program).await.unwrap();
12823 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12824 let sketch_id = sketch_object.id;
12825 let sketch = expect_sketch(sketch_object);
12826 let arc_id = sketch
12828 .segments
12829 .iter()
12830 .find(|&seg_id| {
12831 let obj = frontend.scene_graph.objects.get(seg_id.0);
12832 matches!(
12833 obj.map(|o| &o.kind),
12834 Some(ObjectKind::Segment {
12835 segment: Segment::Arc(_)
12836 })
12837 )
12838 })
12839 .unwrap();
12840
12841 let constraint = Constraint::Diameter(Diameter {
12842 arc: *arc_id,
12843 diameter: Number {
12844 value: 10.0,
12845 units: NumericSuffix::Mm,
12846 },
12847 label_position: None,
12848 source: Default::default(),
12849 });
12850 let (src_delta, scene_delta) = frontend
12851 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12852 .await
12853 .unwrap();
12854 insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12855 assert_eq!(
12856 scene_delta.new_graph.objects.len(),
12857 7, "{:#?}",
12859 scene_delta.new_graph.objects
12860 );
12861
12862 ctx.close().await;
12863 mock_ctx.close().await;
12864 }
12865
12866 #[tokio::test(flavor = "multi_thread")]
12867 async fn test_diameter_single_arc_segment_with_label_position() {
12868 let initial_source = "\
12869sketch(on = XY) {
12870 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12871}
12872";
12873
12874 let program = Program::parse(initial_source).unwrap().0.unwrap();
12875 let mut frontend = FrontendState::new();
12876 let mock_ctx = ExecutorContext::new_mock(None).await;
12877 let version = Version(0);
12878
12879 frontend.program = program.clone();
12880 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12881 frontend.update_state_after_exec(outcome, true);
12882 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12883 let sketch_id = sketch_object.id;
12884 let sketch = expect_sketch(sketch_object);
12885 let arc_id = sketch
12886 .segments
12887 .iter()
12888 .find(|&seg_id| {
12889 let obj = frontend.scene_graph.objects.get(seg_id.0);
12890 matches!(
12891 obj.map(|o| &o.kind),
12892 Some(ObjectKind::Segment {
12893 segment: Segment::Arc(_)
12894 })
12895 )
12896 })
12897 .unwrap();
12898
12899 let label_position = Point2d {
12900 x: Number {
12901 value: 10.0,
12902 units: NumericSuffix::Mm,
12903 },
12904 y: Number {
12905 value: 11.0,
12906 units: NumericSuffix::Mm,
12907 },
12908 };
12909 let constraint = Constraint::Diameter(Diameter {
12910 arc: *arc_id,
12911 diameter: Number {
12912 value: 10.0,
12913 units: NumericSuffix::Mm,
12914 },
12915 label_position: Some(label_position.clone()),
12916 source: Default::default(),
12917 });
12918 let (src_delta, scene_delta) = frontend
12919 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12920 .await
12921 .unwrap();
12922 insta::assert_snapshot!(
12923 "test_diameter_single_arc_segment_with_label_position",
12924 src_delta.text.as_str()
12925 );
12926
12927 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12928 let sketch = expect_sketch(sketch_object);
12929 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].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_edit_diameter_constraint_label_position() {
12943 let initial_source = "\
12944sketch(on = XY) {
12945 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12946 diameter(arc1) == 10mm
12947}
12948";
12949
12950 let program = Program::parse(initial_source).unwrap().0.unwrap();
12951 let mut frontend = FrontendState::new();
12952 let mock_ctx = ExecutorContext::new_mock(None).await;
12953 let version = Version(0);
12954
12955 frontend.program = program.clone();
12956 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12957 frontend.update_state_after_exec(outcome, true);
12958 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12959 let sketch_id = sketch_object.id;
12960 let sketch = expect_sketch(sketch_object);
12961 let constraint_id = sketch.constraints[0];
12962 let label_position = Point2d {
12963 x: Number {
12964 value: 10.0,
12965 units: NumericSuffix::Mm,
12966 },
12967 y: Number {
12968 value: 11.0,
12969 units: NumericSuffix::Mm,
12970 },
12971 };
12972
12973 let (src_delta, scene_delta) = frontend
12974 .edit_distance_constraint_label_position(
12975 &mock_ctx,
12976 version,
12977 sketch_id,
12978 constraint_id,
12979 label_position.clone(),
12980 vec![],
12981 )
12982 .await
12983 .unwrap();
12984 insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12985
12986 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12987 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12988 panic!("Expected constraint object");
12989 };
12990 let Constraint::Diameter(diameter) = constraint else {
12991 panic!("Expected diameter constraint");
12992 };
12993 assert_eq!(diameter.label_position, Some(label_position));
12994
12995 mock_ctx.close().await;
12996 }
12997
12998 #[tokio::test(flavor = "multi_thread")]
12999 async fn test_diameter_error_cases() {
13000 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13001 let mock_ctx = ExecutorContext::new_mock(None).await;
13002 let version = Version(0);
13003
13004 let initial_source_point = "\
13006sketch(on = XY) {
13007 point(at = [var 1, var 2])
13008}
13009";
13010 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
13011 let mut frontend_point = FrontendState::new();
13012 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
13013 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
13014 let sketch_id_point = sketch_object_point.id;
13015 let sketch_point = expect_sketch(sketch_object_point);
13016 let point_id = *sketch_point.segments.first().unwrap();
13017
13018 let constraint_point = Constraint::Diameter(Diameter {
13019 arc: point_id,
13020 diameter: Number {
13021 value: 10.0,
13022 units: NumericSuffix::Mm,
13023 },
13024 label_position: None,
13025 source: Default::default(),
13026 });
13027 let result_point = frontend_point
13028 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
13029 .await;
13030 assert!(result_point.is_err(), "Single point should error for diameter");
13031
13032 let initial_source_line = "\
13034sketch(on = XY) {
13035 line(start = [var 1, var 2], end = [var 3, var 4])
13036}
13037";
13038 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
13039 let mut frontend_line = FrontendState::new();
13040 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
13041 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
13042 let sketch_id_line = sketch_object_line.id;
13043 let sketch_line = expect_sketch(sketch_object_line);
13044 let line_id = *sketch_line.segments.first().unwrap();
13045
13046 let constraint_line = Constraint::Diameter(Diameter {
13047 arc: line_id,
13048 diameter: Number {
13049 value: 10.0,
13050 units: NumericSuffix::Mm,
13051 },
13052 label_position: None,
13053 source: Default::default(),
13054 });
13055 let result_line = frontend_line
13056 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
13057 .await;
13058 assert!(result_line.is_err(), "Single line segment should error for diameter");
13059
13060 ctx.close().await;
13061 mock_ctx.close().await;
13062 }
13063
13064 #[tokio::test(flavor = "multi_thread")]
13065 async fn test_line_horizontal() {
13066 let initial_source = "\
13067sketch(on = XY) {
13068 line(start = [var 1, var 2], end = [var 3, var 4])
13069}
13070";
13071
13072 let program = Program::parse(initial_source).unwrap().0.unwrap();
13073
13074 let mut frontend = FrontendState::new();
13075
13076 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13077 let mock_ctx = ExecutorContext::new_mock(None).await;
13078 let version = Version(0);
13079
13080 frontend.hack_set_program(&ctx, program).await.unwrap();
13081 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13082 let sketch_id = sketch_object.id;
13083 let sketch = expect_sketch(sketch_object);
13084 let line1_id = *sketch.segments.get(2).unwrap();
13085
13086 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13087 let (src_delta, scene_delta) = frontend
13088 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13089 .await
13090 .unwrap();
13091 insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13092 assert_eq!(
13093 scene_delta.new_graph.objects.len(),
13094 6,
13095 "{:#?}",
13096 scene_delta.new_graph.objects
13097 );
13098
13099 ctx.close().await;
13100 mock_ctx.close().await;
13101 }
13102
13103 #[tokio::test(flavor = "multi_thread")]
13104 async fn test_control_point_spline_edge_horizontal() {
13105 let initial_source = "\
13106@settings(experimentalFeatures = allow)
13107splineSketch = sketch(on = XY) {
13108 controlPointSpline1 = controlPointSpline(points = [
13109 [var 0mm, var 0mm],
13110 [var 10mm, var 20mm],
13111 [var 20mm, var 0mm],
13112 ])
13113}
13114";
13115
13116 let program = Program::parse(initial_source).unwrap().0.unwrap();
13117
13118 let mut frontend = FrontendState::new();
13119
13120 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13121 let mock_ctx = ExecutorContext::new_mock(None).await;
13122 let version = Version(0);
13123
13124 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13125 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13126 let sketch_id = sketch_object.id;
13127 let sketch = expect_sketch(sketch_object);
13128 let spline_id = sketch
13129 .segments
13130 .iter()
13131 .copied()
13132 .find(|seg_id| {
13133 matches!(
13134 &frontend.scene_graph.objects[seg_id.0].kind,
13135 ObjectKind::Segment {
13136 segment: Segment::ControlPointSpline(_)
13137 }
13138 )
13139 })
13140 .expect("Expected a control point spline segment in sketch");
13141 let edge_id = frontend
13142 .scene_graph
13143 .objects
13144 .iter()
13145 .find_map(|obj| match &obj.kind {
13146 ObjectKind::Segment {
13147 segment: Segment::Line(line),
13148 } if line.owner == Some(spline_id) => Some(obj.id),
13149 _ => None,
13150 })
13151 .expect("Expected an owned control-polygon edge");
13152
13153 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13154 let (src_delta, _) = frontend
13155 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13156 .await
13157 .unwrap();
13158 assert!(
13159 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13160 "Expected horizontal constraint on spline edge, got: {}",
13161 src_delta.text
13162 );
13163
13164 ctx.close().await;
13165 mock_ctx.close().await;
13166 }
13167
13168 #[tokio::test(flavor = "multi_thread")]
13169 async fn test_control_point_spline_edge_angle() {
13170 let initial_source = "\
13171@settings(experimentalFeatures = allow)
13172splineSketch = sketch(on = XY) {
13173 controlPointSpline1 = controlPointSpline(points = [
13174 [var 0mm, var 0mm],
13175 [var 10mm, var 20mm],
13176 [var 20mm, var 0mm],
13177 ])
13178
13179 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13180}
13181";
13182
13183 let program = Program::parse(initial_source).unwrap().0.unwrap();
13184
13185 let mut frontend = FrontendState::new();
13186
13187 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13188 let mock_ctx = ExecutorContext::new_mock(None).await;
13189 let version = Version(0);
13190
13191 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13192 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13193 let sketch_id = sketch_object.id;
13194 let sketch = expect_sketch(sketch_object);
13195 let spline_id = sketch
13196 .segments
13197 .iter()
13198 .copied()
13199 .find(|seg_id| {
13200 matches!(
13201 &frontend.scene_graph.objects[seg_id.0].kind,
13202 ObjectKind::Segment {
13203 segment: Segment::ControlPointSpline(_)
13204 }
13205 )
13206 })
13207 .expect("Expected a control point spline segment in sketch");
13208 let edge_id = frontend
13209 .scene_graph
13210 .objects
13211 .iter()
13212 .find_map(|obj| match &obj.kind {
13213 ObjectKind::Segment {
13214 segment: Segment::Line(line),
13215 } if line.owner == Some(spline_id) => Some(obj.id),
13216 _ => None,
13217 })
13218 .expect("Expected an owned control-polygon edge");
13219 let line1_id = frontend
13220 .scene_graph
13221 .objects
13222 .iter()
13223 .find_map(|obj| match &obj.kind {
13224 ObjectKind::Segment {
13225 segment: Segment::Line(line),
13226 } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
13227 _ => None,
13228 })
13229 .or_else(|| {
13230 sketch.segments.iter().copied().find(|seg_id| {
13231 matches!(
13232 &frontend.scene_graph.objects[seg_id.0].kind,
13233 ObjectKind::Segment {
13234 segment: Segment::Line(line),
13235 } if line.owner.is_none()
13236 )
13237 })
13238 })
13239 .expect("Expected a standalone line segment in sketch");
13240
13241 let constraint = Constraint::Angle(Angle {
13242 lines: vec![line1_id, edge_id],
13243 angle: Number {
13244 value: 30.0,
13245 units: NumericSuffix::Deg,
13246 },
13247 sector: None,
13248 inverse: None,
13249 label_position: None,
13250 source: Default::default(),
13251 });
13252 let (src_delta, _) = frontend
13253 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13254 .await
13255 .unwrap();
13256 assert!(
13257 src_delta
13258 .text
13259 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13260 "Expected angle constraint on spline edge, got: {}",
13261 src_delta.text
13262 );
13263
13264 ctx.close().await;
13265 mock_ctx.close().await;
13266 }
13267
13268 #[tokio::test(flavor = "multi_thread")]
13269 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13270 let initial_source = "\
13271@settings(experimentalFeatures = allow)
13272splineSketch = sketch(on = XY) {
13273 spline1 = controlPointSpline(points = [
13274 [var 0mm, var 0mm],
13275 [var 10mm, var 20mm],
13276 [var 20mm, var 0mm],
13277 ])
13278 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13279 coincident([spline1.controls[1], spline1.edges[0]])
13280 coincident([spline1.controls[0], line1])
13281}
13282";
13283
13284 let program = Program::parse(initial_source).unwrap().0.unwrap();
13285
13286 let mut frontend = FrontendState::new();
13287
13288 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13289 let mock_ctx = ExecutorContext::new_mock(None).await;
13290
13291 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13292
13293 let ui_scene_graph = frontend.scene_graph_for_ui();
13294 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13295 let sketch = expect_sketch(sketch_object);
13296
13297 assert_eq!(
13298 sketch.constraints.len(),
13299 1,
13300 "Expected only the external coincident constraint to remain visible in the UI scene graph"
13301 );
13302
13303 let visible_constraints = ui_scene_graph
13304 .objects
13305 .iter()
13306 .filter_map(|object| match &object.kind {
13307 ObjectKind::Constraint {
13308 constraint: Constraint::Coincident(coincident),
13309 } => Some(coincident.clone()),
13310 _ => None,
13311 })
13312 .collect::<Vec<_>>();
13313
13314 assert_eq!(
13315 visible_constraints.len(),
13316 1,
13317 "Expected only one coincident constraint object in the UI scene graph"
13318 );
13319 assert_eq!(
13320 visible_constraints[0].get_segments().len(),
13321 2,
13322 "Expected the remaining visible coincident constraint to reference two segments"
13323 );
13324
13325 ctx.close().await;
13326 mock_ctx.close().await;
13327 }
13328
13329 #[tokio::test(flavor = "multi_thread")]
13330 async fn test_edit_control_point_spline_can_append_control_point() {
13331 let initial_source = "\
13332@settings(experimentalFeatures = allow)
13333splineSketch = sketch(on = XY) {
13334 controlPointSpline(points = [
13335 [var 0mm, var 0mm],
13336 [var 10mm, var 20mm],
13337 [var 20mm, var 0mm],
13338 ])
13339}
13340";
13341
13342 let program = Program::parse(initial_source).unwrap().0.unwrap();
13343
13344 let mut frontend = FrontendState::new();
13345
13346 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13347 let mock_ctx = ExecutorContext::new_mock(None).await;
13348 let version = Version(0);
13349
13350 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13351 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13352 let sketch_id = sketch_object.id;
13353 let sketch = expect_sketch(sketch_object);
13354 let spline_id = sketch
13355 .segments
13356 .iter()
13357 .copied()
13358 .find(|seg_id| {
13359 matches!(
13360 &frontend.scene_graph.objects[seg_id.0].kind,
13361 ObjectKind::Segment {
13362 segment: Segment::ControlPointSpline(_)
13363 }
13364 )
13365 })
13366 .expect("Expected a control point spline segment in sketch");
13367
13368 let ctor = ControlPointSplineCtor {
13369 points: vec![
13370 Point2d {
13371 x: Expr::Var(Number {
13372 value: 0.0,
13373 units: NumericSuffix::Mm,
13374 }),
13375 y: Expr::Var(Number {
13376 value: 0.0,
13377 units: NumericSuffix::Mm,
13378 }),
13379 },
13380 Point2d {
13381 x: Expr::Var(Number {
13382 value: 10.0,
13383 units: NumericSuffix::Mm,
13384 }),
13385 y: Expr::Var(Number {
13386 value: 20.0,
13387 units: NumericSuffix::Mm,
13388 }),
13389 },
13390 Point2d {
13391 x: Expr::Var(Number {
13392 value: 20.0,
13393 units: NumericSuffix::Mm,
13394 }),
13395 y: Expr::Var(Number {
13396 value: 0.0,
13397 units: NumericSuffix::Mm,
13398 }),
13399 },
13400 Point2d {
13401 x: Expr::Var(Number {
13402 value: 30.0,
13403 units: NumericSuffix::Mm,
13404 }),
13405 y: Expr::Var(Number {
13406 value: 10.0,
13407 units: NumericSuffix::Mm,
13408 }),
13409 },
13410 ],
13411 construction: None,
13412 };
13413
13414 let segments = vec![ExistingSegmentCtor {
13415 id: spline_id,
13416 ctor: SegmentCtor::ControlPointSpline(ctor),
13417 }];
13418 let (src_delta, scene_delta) = frontend
13419 .edit_segments(&mock_ctx, version, sketch_id, segments)
13420 .await
13421 .unwrap();
13422
13423 assert!(
13424 src_delta.text.contains("[var 30mm, var 10mm]"),
13425 "Expected appended spline control point in source, got: {}",
13426 src_delta.text
13427 );
13428
13429 assert!(
13430 scene_delta.invalidates_ids,
13431 "Expected appending a spline control point to invalidate ids"
13432 );
13433 let updated_spline = scene_delta
13434 .new_graph
13435 .objects
13436 .iter()
13437 .find_map(|obj| match &obj.kind {
13438 ObjectKind::Segment {
13439 segment: Segment::ControlPointSpline(updated_spline),
13440 } if updated_spline.controls.len() == 4 => Some(updated_spline),
13441 _ => None,
13442 })
13443 .expect("Expected edited scene graph to contain a four-point control point spline");
13444 assert_eq!(
13445 updated_spline.controls.len(),
13446 4,
13447 "Expected edited spline to expose four control points"
13448 );
13449
13450 ctx.close().await;
13451 mock_ctx.close().await;
13452 }
13453
13454 #[tokio::test(flavor = "multi_thread")]
13455 async fn test_line_vertical() {
13456 let initial_source = "\
13457sketch(on = XY) {
13458 line(start = [var 1, var 2], end = [var 3, var 4])
13459}
13460";
13461
13462 let program = Program::parse(initial_source).unwrap().0.unwrap();
13463
13464 let mut frontend = FrontendState::new();
13465
13466 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13467 let mock_ctx = ExecutorContext::new_mock(None).await;
13468 let version = Version(0);
13469
13470 frontend.hack_set_program(&ctx, program).await.unwrap();
13471 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13472 let sketch_id = sketch_object.id;
13473 let sketch = expect_sketch(sketch_object);
13474 let line1_id = *sketch.segments.get(2).unwrap();
13475
13476 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13477 let (src_delta, scene_delta) = frontend
13478 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13479 .await
13480 .unwrap();
13481 insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13482 assert_eq!(
13483 scene_delta.new_graph.objects.len(),
13484 6,
13485 "{:#?}",
13486 scene_delta.new_graph.objects
13487 );
13488
13489 ctx.close().await;
13490 mock_ctx.close().await;
13491 }
13492
13493 #[tokio::test(flavor = "multi_thread")]
13494 async fn test_points_vertical() {
13495 let initial_source = "\
13496sketch001 = sketch(on = XY) {
13497 p0 = point(at = [var -2.23mm, var 3.1mm])
13498 pf = point(at = [4, 4])
13499}
13500";
13501
13502 let program = Program::parse(initial_source).unwrap().0.unwrap();
13503
13504 let mut frontend = FrontendState::new();
13505
13506 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13507 let mock_ctx = ExecutorContext::new_mock(None).await;
13508 let version = Version(0);
13509
13510 frontend.hack_set_program(&ctx, program).await.unwrap();
13511 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13512 let sketch_id = sketch_object.id;
13513 let sketch = expect_sketch(sketch_object);
13514 let point_ids = vec![
13515 sketch.segments.first().unwrap().to_owned(),
13516 sketch.segments.get(1).unwrap().to_owned(),
13517 ];
13518
13519 let constraint = Constraint::Vertical(Vertical::Points {
13520 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13521 });
13522 let (src_delta, scene_delta) = frontend
13523 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13524 .await
13525 .unwrap();
13526 insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13527 assert_eq!(
13528 scene_delta.new_graph.objects.len(),
13529 5,
13530 "{:#?}",
13531 scene_delta.new_graph.objects
13532 );
13533
13534 ctx.close().await;
13535 mock_ctx.close().await;
13536 }
13537
13538 #[tokio::test(flavor = "multi_thread")]
13539 async fn test_points_horizontal() {
13540 let initial_source = "\
13541sketch001 = sketch(on = XY) {
13542 p0 = point(at = [var -2.23mm, var 3.1mm])
13543 pf = point(at = [4, 4])
13544}
13545";
13546
13547 let program = Program::parse(initial_source).unwrap().0.unwrap();
13548
13549 let mut frontend = FrontendState::new();
13550
13551 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13552 let mock_ctx = ExecutorContext::new_mock(None).await;
13553 let version = Version(0);
13554
13555 frontend.hack_set_program(&ctx, program).await.unwrap();
13556 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13557 let sketch_id = sketch_object.id;
13558 let sketch = expect_sketch(sketch_object);
13559 let point_ids = vec![
13560 sketch.segments.first().unwrap().to_owned(),
13561 sketch.segments.get(1).unwrap().to_owned(),
13562 ];
13563
13564 let constraint = Constraint::Horizontal(Horizontal::Points {
13565 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13566 });
13567 let (src_delta, scene_delta) = frontend
13568 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13569 .await
13570 .unwrap();
13571 insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13572 assert_eq!(
13573 scene_delta.new_graph.objects.len(),
13574 5,
13575 "{:#?}",
13576 scene_delta.new_graph.objects
13577 );
13578
13579 ctx.close().await;
13580 mock_ctx.close().await;
13581 }
13582
13583 #[tokio::test(flavor = "multi_thread")]
13584 async fn test_point_horizontal_with_origin() {
13585 let initial_source = "\
13586sketch001 = sketch(on = XY) {
13587 p0 = point(at = [var -2.23mm, var 3.1mm])
13588}
13589";
13590
13591 let program = Program::parse(initial_source).unwrap().0.unwrap();
13592
13593 let mut frontend = FrontendState::new();
13594
13595 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13596 let mock_ctx = ExecutorContext::new_mock(None).await;
13597 let version = Version(0);
13598
13599 frontend.hack_set_program(&ctx, program).await.unwrap();
13600 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13601 let sketch_id = sketch_object.id;
13602 let sketch = expect_sketch(sketch_object);
13603 let point_id = *sketch.segments.first().unwrap();
13604
13605 let constraint = Constraint::Horizontal(Horizontal::Points {
13606 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13607 });
13608 let (src_delta, scene_delta) = frontend
13609 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13610 .await
13611 .unwrap();
13612 insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13613 assert_eq!(
13614 scene_delta.new_graph.objects.len(),
13615 4,
13616 "{:#?}",
13617 scene_delta.new_graph.objects
13618 );
13619
13620 ctx.close().await;
13621 mock_ctx.close().await;
13622 }
13623
13624 #[tokio::test(flavor = "multi_thread")]
13625 async fn test_lines_equal_length() {
13626 let initial_source = "\
13627sketch(on = XY) {
13628 line(start = [var 1, var 2], end = [var 3, var 4])
13629 line(start = [var 5, var 6], end = [var 7, var 8])
13630}
13631";
13632
13633 let program = Program::parse(initial_source).unwrap().0.unwrap();
13634
13635 let mut frontend = FrontendState::new();
13636
13637 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13638 let mock_ctx = ExecutorContext::new_mock(None).await;
13639 let version = Version(0);
13640
13641 frontend.hack_set_program(&ctx, program).await.unwrap();
13642 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13643 let sketch_id = sketch_object.id;
13644 let sketch = expect_sketch(sketch_object);
13645 let line1_id = *sketch.segments.get(2).unwrap();
13646 let line2_id = *sketch.segments.get(5).unwrap();
13647
13648 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13649 lines: vec![line1_id, line2_id],
13650 });
13651 let (src_delta, scene_delta) = frontend
13652 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13653 .await
13654 .unwrap();
13655 insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13656 assert_eq!(
13657 scene_delta.new_graph.objects.len(),
13658 9,
13659 "{:#?}",
13660 scene_delta.new_graph.objects
13661 );
13662
13663 ctx.close().await;
13664 mock_ctx.close().await;
13665 }
13666
13667 #[tokio::test(flavor = "multi_thread")]
13668 async fn test_add_constraint_multi_line_equal_length() {
13669 let initial_source = "\
13670sketch(on = XY) {
13671 line(start = [var 1, var 2], end = [var 3, var 4])
13672 line(start = [var 5, var 6], end = [var 7, var 8])
13673 line(start = [var 9, var 10], end = [var 11, var 12])
13674}
13675";
13676
13677 let program = Program::parse(initial_source).unwrap().0.unwrap();
13678
13679 let mut frontend = FrontendState::new();
13680 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13681 let mock_ctx = ExecutorContext::new_mock(None).await;
13682 let version = Version(0);
13683
13684 frontend.hack_set_program(&ctx, program).await.unwrap();
13685 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13686 let sketch_id = sketch_object.id;
13687 let sketch = expect_sketch(sketch_object);
13688 let line1_id = *sketch.segments.get(2).unwrap();
13689 let line2_id = *sketch.segments.get(5).unwrap();
13690 let line3_id = *sketch.segments.get(8).unwrap();
13691
13692 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13693 lines: vec![line1_id, line2_id, line3_id],
13694 });
13695 let (src_delta, scene_delta) = frontend
13696 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13697 .await
13698 .unwrap();
13699 insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13700 let constraints = scene_delta
13701 .new_graph
13702 .objects
13703 .iter()
13704 .filter_map(|obj| {
13705 let ObjectKind::Constraint { constraint } = &obj.kind else {
13706 return None;
13707 };
13708 Some(constraint)
13709 })
13710 .collect::<Vec<_>>();
13711
13712 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13713 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13714 panic!("expected equal length constraint, got {:?}", constraints[0]);
13715 };
13716 assert_eq!(lines_equal_length.lines.len(), 3);
13717
13718 ctx.close().await;
13719 mock_ctx.close().await;
13720 }
13721
13722 #[tokio::test(flavor = "multi_thread")]
13723 async fn test_lines_parallel() {
13724 let initial_source = "\
13725sketch(on = XY) {
13726 line(start = [var 1, var 2], end = [var 3, var 4])
13727 line(start = [var 5, var 6], end = [var 7, var 8])
13728}
13729";
13730
13731 let program = Program::parse(initial_source).unwrap().0.unwrap();
13732
13733 let mut frontend = FrontendState::new();
13734
13735 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13736 let mock_ctx = ExecutorContext::new_mock(None).await;
13737 let version = Version(0);
13738
13739 frontend.hack_set_program(&ctx, program).await.unwrap();
13740 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13741 let sketch_id = sketch_object.id;
13742 let sketch = expect_sketch(sketch_object);
13743 let line1_id = *sketch.segments.get(2).unwrap();
13744 let line2_id = *sketch.segments.get(5).unwrap();
13745
13746 let constraint = Constraint::Parallel(Parallel {
13747 lines: vec![line1_id, line2_id],
13748 });
13749 let (src_delta, scene_delta) = frontend
13750 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13751 .await
13752 .unwrap();
13753 insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13754 assert_eq!(
13755 scene_delta.new_graph.objects.len(),
13756 9,
13757 "{:#?}",
13758 scene_delta.new_graph.objects
13759 );
13760
13761 ctx.close().await;
13762 mock_ctx.close().await;
13763 }
13764
13765 #[tokio::test(flavor = "multi_thread")]
13766 async fn test_lines_parallel_multiline() {
13767 let initial_source = "\
13768sketch(on = XY) {
13769 line(start = [var 1, var 2], end = [var 3, var 4])
13770 line(start = [var 5, var 6], end = [var 7, var 8])
13771 line(start = [var 9, var 10], end = [var 11, var 12])
13772}
13773";
13774
13775 let program = Program::parse(initial_source).unwrap().0.unwrap();
13776
13777 let mut frontend = FrontendState::new();
13778
13779 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13780 let mock_ctx = ExecutorContext::new_mock(None).await;
13781 let version = Version(0);
13782
13783 frontend.hack_set_program(&ctx, program).await.unwrap();
13784 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13785 let sketch_id = sketch_object.id;
13786 let sketch = expect_sketch(sketch_object);
13787 let line1_id = *sketch.segments.get(2).unwrap();
13788 let line2_id = *sketch.segments.get(5).unwrap();
13789 let line3_id = *sketch.segments.get(8).unwrap();
13790
13791 let constraint = Constraint::Parallel(Parallel {
13792 lines: vec![line1_id, line2_id, line3_id],
13793 });
13794 let (src_delta, scene_delta) = frontend
13795 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13796 .await
13797 .unwrap();
13798 insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13799
13800 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13801 let sketch = expect_sketch(sketch_object);
13802 assert_eq!(sketch.constraints.len(), 1);
13803
13804 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13805 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13806 panic!("Expected constraint object");
13807 };
13808 let Constraint::Parallel(parallel) = constraint else {
13809 panic!("Expected parallel constraint");
13810 };
13811 assert_eq!(parallel.lines.len(), 3);
13812
13813 ctx.close().await;
13814 mock_ctx.close().await;
13815 }
13816
13817 #[tokio::test(flavor = "multi_thread")]
13818 async fn test_lines_perpendicular() {
13819 let initial_source = "\
13820sketch(on = XY) {
13821 line(start = [var 1, var 2], end = [var 3, var 4])
13822 line(start = [var 5, var 6], end = [var 7, var 8])
13823}
13824";
13825
13826 let program = Program::parse(initial_source).unwrap().0.unwrap();
13827
13828 let mut frontend = FrontendState::new();
13829
13830 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13831 let mock_ctx = ExecutorContext::new_mock(None).await;
13832 let version = Version(0);
13833
13834 frontend.hack_set_program(&ctx, program).await.unwrap();
13835 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13836 let sketch_id = sketch_object.id;
13837 let sketch = expect_sketch(sketch_object);
13838 let line1_id = *sketch.segments.get(2).unwrap();
13839 let line2_id = *sketch.segments.get(5).unwrap();
13840
13841 let constraint = Constraint::Perpendicular(Perpendicular {
13842 lines: vec![line1_id, line2_id],
13843 });
13844 let (src_delta, scene_delta) = frontend
13845 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13846 .await
13847 .unwrap();
13848 insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13849 assert_eq!(
13850 scene_delta.new_graph.objects.len(),
13851 9,
13852 "{:#?}",
13853 scene_delta.new_graph.objects
13854 );
13855
13856 ctx.close().await;
13857 mock_ctx.close().await;
13858 }
13859
13860 #[tokio::test(flavor = "multi_thread")]
13861 async fn test_lines_angle() {
13862 let initial_source = "\
13863sketch(on = XY) {
13864 line(start = [var 1, var 2], end = [var 3, var 4])
13865 line(start = [var 5, var 6], end = [var 7, var 8])
13866}
13867";
13868
13869 let program = Program::parse(initial_source).unwrap().0.unwrap();
13870
13871 let mut frontend = FrontendState::new();
13872
13873 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13874 let mock_ctx = ExecutorContext::new_mock(None).await;
13875 let version = Version(0);
13876
13877 frontend.hack_set_program(&ctx, program).await.unwrap();
13878 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13879 let sketch_id = sketch_object.id;
13880 let sketch = expect_sketch(sketch_object);
13881 let line1_id = *sketch.segments.get(2).unwrap();
13882 let line2_id = *sketch.segments.get(5).unwrap();
13883
13884 let constraint = Constraint::Angle(Angle {
13885 lines: vec![line1_id, line2_id],
13886 angle: Number {
13887 value: 30.0,
13888 units: NumericSuffix::Deg,
13889 },
13890 sector: None,
13891 inverse: None,
13892 label_position: None,
13893 source: Default::default(),
13894 });
13895 let (src_delta, scene_delta) = frontend
13896 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13897 .await
13898 .unwrap();
13899 insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13900 assert_eq!(
13901 scene_delta.new_graph.objects.len(),
13902 9,
13903 "{:#?}",
13904 scene_delta.new_graph.objects
13905 );
13906
13907 ctx.close().await;
13908 mock_ctx.close().await;
13909 }
13910
13911 #[tokio::test(flavor = "multi_thread")]
13912 async fn test_lines_angle_with_sector_uses_angle_dimension() {
13913 let initial_source = "\
13914sketch(on = XY) {
13915 line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13916 line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13917}
13918";
13919
13920 let program = Program::parse(initial_source).unwrap().0.unwrap();
13921
13922 let mut frontend = FrontendState::new();
13923
13924 let mock_ctx = ExecutorContext::new_mock(None).await;
13925 let version = Version(0);
13926
13927 frontend.program = program.clone();
13928 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13929 frontend.update_state_after_exec(outcome, true);
13930 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13931 let sketch_id = sketch_object.id;
13932 let sketch = expect_sketch(sketch_object);
13933 let line1_id = *sketch.segments.get(2).unwrap();
13934 let line2_id = *sketch.segments.get(5).unwrap();
13935
13936 let constraint = Constraint::Angle(Angle {
13937 lines: vec![line1_id, line2_id],
13938 angle: Number {
13939 value: 270.0,
13940 units: NumericSuffix::Deg,
13941 },
13942 sector: Some(1),
13943 inverse: Some(true),
13944 label_position: Some(Point2d {
13945 x: Number {
13946 value: -0.73,
13947 units: NumericSuffix::Mm,
13948 },
13949 y: Number {
13950 value: 0.75,
13951 units: NumericSuffix::Mm,
13952 },
13953 }),
13954 source: Default::default(),
13955 });
13956 let (src_delta, _) = frontend
13957 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13958 .await
13959 .unwrap();
13960 assert_eq!(
13961 src_delta.text.as_str(),
13962 "\
13963sketch(on = XY) {
13964 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13965 line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13966 angleDimension(
13967 lines = [line1, line2],
13968 sector = 1,
13969 inverse = true,
13970 labelPosition = [-0.73mm, 0.75mm],
13971) == 270deg
13972}
13973"
13974 );
13975
13976 mock_ctx.close().await;
13977 }
13978
13979 #[tokio::test(flavor = "multi_thread")]
13980 async fn test_segments_tangent() {
13981 let initial_source = "\
13982sketch(on = XY) {
13983 line(start = [var 1, var 2], end = [var 3, var 4])
13984 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13985}
13986";
13987
13988 let program = Program::parse(initial_source).unwrap().0.unwrap();
13989
13990 let mut frontend = FrontendState::new();
13991
13992 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13993 let mock_ctx = ExecutorContext::new_mock(None).await;
13994 let version = Version(0);
13995
13996 frontend.hack_set_program(&ctx, program).await.unwrap();
13997 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13998 let sketch_id = sketch_object.id;
13999 let sketch = expect_sketch(sketch_object);
14000 let line1_id = *sketch.segments.get(2).unwrap();
14001 let arc1_id = *sketch.segments.get(6).unwrap();
14002
14003 let constraint = Constraint::Tangent(Tangent {
14004 input: vec![line1_id, arc1_id],
14005 });
14006 let (src_delta, scene_delta) = frontend
14007 .add_constraint(&mock_ctx, version, sketch_id, constraint)
14008 .await
14009 .unwrap();
14010 insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
14011 assert_eq!(
14012 scene_delta.new_graph.objects.len(),
14013 10,
14014 "{:#?}",
14015 scene_delta.new_graph.objects
14016 );
14017
14018 ctx.close().await;
14019 mock_ctx.close().await;
14020 }
14021
14022 #[tokio::test(flavor = "multi_thread")]
14023 async fn test_point_midpoint() {
14024 let initial_source = "\
14025sketch(on = XY) {
14026 point(at = [var 1, var 1])
14027 line(start = [var 0, var 0], end = [var 6, var 4])
14028}
14029";
14030
14031 let program = Program::parse(initial_source).unwrap().0.unwrap();
14032
14033 let mut frontend = FrontendState::new();
14034
14035 let ctx = ExecutorContext::new_mock(None).await;
14036 let version = Version(0);
14037
14038 frontend.program = program.clone();
14039 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14040 frontend.update_state_after_exec(outcome, true);
14041 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14042 let sketch_id = sketch_object.id;
14043 let sketch = expect_sketch(sketch_object);
14044 let point_id = *sketch.segments.first().unwrap();
14045 let line_id = *sketch.segments.get(3).unwrap();
14046
14047 let constraint = Constraint::Midpoint(Midpoint {
14048 point: ConstraintSegment::from(point_id),
14049 segment: line_id,
14050 });
14051 let (src_delta, scene_delta) = frontend
14052 .add_constraint(&ctx, version, sketch_id, constraint)
14053 .await
14054 .unwrap();
14055 insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
14056 assert_eq!(
14057 scene_delta.new_graph.objects.len(),
14058 7,
14059 "{:#?}",
14060 scene_delta.new_graph.objects
14061 );
14062
14063 ctx.close().await;
14064 }
14065
14066 #[tokio::test(flavor = "multi_thread")]
14067 async fn test_segments_symmetric() {
14068 let initial_source = "\
14069sketch(on = XY) {
14070 line(start = [var 0, var 0], end = [var 0, var 4])
14071 line(start = [var 4, var 0], end = [var 4, var 4])
14072 line(start = [var 2, var -1], end = [var 2, var 5])
14073}
14074";
14075
14076 let program = Program::parse(initial_source).unwrap().0.unwrap();
14077
14078 let mut frontend = FrontendState::new();
14079
14080 let ctx = ExecutorContext::new_mock(None).await;
14081 let version = Version(0);
14082
14083 frontend.program = program.clone();
14084 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14085 frontend.update_state_after_exec(outcome, true);
14086 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14087 let sketch_id = sketch_object.id;
14088 let sketch = expect_sketch(sketch_object);
14089 let line1_id = *sketch.segments.get(2).unwrap();
14090 let line2_id = *sketch.segments.get(5).unwrap();
14091 let axis_id = *sketch.segments.get(8).unwrap();
14092
14093 let constraint = Constraint::Symmetric(Symmetric {
14094 input: vec![line1_id, line2_id],
14095 axis: axis_id,
14096 });
14097 let (src_delta, scene_delta) = frontend
14098 .add_constraint(&ctx, version, sketch_id, constraint)
14099 .await
14100 .unwrap();
14101 insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14102 assert_eq!(
14103 scene_delta.new_graph.objects.len(),
14104 12,
14105 "{:#?}",
14106 scene_delta.new_graph.objects
14107 );
14108
14109 ctx.close().await;
14110 }
14111
14112 #[tokio::test(flavor = "multi_thread")]
14113 async fn test_point_arc_midpoint() {
14114 let initial_source = "\
14115sketch(on = XY) {
14116 point(at = [var 6, var 3])
14117 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14118}
14119";
14120
14121 let program = Program::parse(initial_source).unwrap().0.unwrap();
14122
14123 let mut frontend = FrontendState::new();
14124
14125 let ctx = ExecutorContext::new_mock(None).await;
14126 let version = Version(0);
14127
14128 frontend.program = program.clone();
14129 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14130 frontend.update_state_after_exec(outcome, true);
14131 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14132 let sketch_id = sketch_object.id;
14133 let sketch = expect_sketch(sketch_object);
14134 let point_id = *sketch.segments.first().unwrap();
14135 let arc_id = *sketch.segments.get(4).unwrap();
14136
14137 let constraint = Constraint::Midpoint(Midpoint {
14138 point: ConstraintSegment::from(point_id),
14139 segment: arc_id,
14140 });
14141 let (src_delta, scene_delta) = frontend
14142 .add_constraint(&ctx, version, sketch_id, constraint)
14143 .await
14144 .unwrap();
14145 insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14146 assert_eq!(
14147 scene_delta.new_graph.objects.len(),
14148 8,
14149 "{:#?}",
14150 scene_delta.new_graph.objects
14151 );
14152
14153 ctx.close().await;
14154 }
14155
14156 #[tokio::test(flavor = "multi_thread")]
14157 async fn test_origin_line_midpoint() {
14158 let initial_source = "\
14159sketch(on = XY) {
14160 line(start = [var 0, var 0], end = [var 6, var 4])
14161}
14162";
14163
14164 let program = Program::parse(initial_source).unwrap().0.unwrap();
14165
14166 let mut frontend = FrontendState::new();
14167
14168 let ctx = ExecutorContext::new_mock(None).await;
14169 let version = Version(0);
14170
14171 frontend.program = program.clone();
14172 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14173 frontend.update_state_after_exec(outcome, true);
14174 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14175 let sketch_id = sketch_object.id;
14176 let sketch = expect_sketch(sketch_object);
14177 let line_id = *sketch.segments.get(2).unwrap();
14178
14179 let constraint = Constraint::Midpoint(Midpoint {
14180 point: ConstraintSegment::ORIGIN,
14181 segment: line_id,
14182 });
14183 let (src_delta, scene_delta) = frontend
14184 .add_constraint(&ctx, version, sketch_id, constraint)
14185 .await
14186 .unwrap();
14187 insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14188 assert_eq!(
14189 scene_delta.new_graph.objects.len(),
14190 6,
14191 "{:#?}",
14192 scene_delta.new_graph.objects
14193 );
14194
14195 ctx.close().await;
14196 }
14197
14198 #[tokio::test(flavor = "multi_thread")]
14199 async fn test_origin_arc_midpoint() {
14200 let initial_source = "\
14201sketch(on = XY) {
14202 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14203}
14204";
14205
14206 let program = Program::parse(initial_source).unwrap().0.unwrap();
14207
14208 let mut frontend = FrontendState::new();
14209
14210 let ctx = ExecutorContext::new_mock(None).await;
14211 let version = Version(0);
14212
14213 frontend.program = program.clone();
14214 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14215 frontend.update_state_after_exec(outcome, true);
14216 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14217 let sketch_id = sketch_object.id;
14218 let sketch = expect_sketch(sketch_object);
14219 let arc_id = *sketch.segments.get(3).unwrap();
14220
14221 let constraint = Constraint::Midpoint(Midpoint {
14222 point: ConstraintSegment::ORIGIN,
14223 segment: arc_id,
14224 });
14225 let (src_delta, scene_delta) = frontend
14226 .add_constraint(&ctx, version, sketch_id, constraint)
14227 .await
14228 .unwrap();
14229 insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14230 assert_eq!(
14231 scene_delta.new_graph.objects.len(),
14232 7,
14233 "{:#?}",
14234 scene_delta.new_graph.objects
14235 );
14236
14237 ctx.close().await;
14238 }
14239
14240 #[tokio::test(flavor = "multi_thread")]
14241 async fn test_segments_symmetric_arcs() {
14242 let initial_source = "\
14243sketch(on = XY) {
14244 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14245 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14246 line(start = [var 0, var -10], end = [var 0, var 10])
14247}
14248";
14249
14250 let program = Program::parse(initial_source).unwrap().0.unwrap();
14251
14252 let mut frontend = FrontendState::new();
14253
14254 let ctx = ExecutorContext::new_mock(None).await;
14255 let version = Version(0);
14256
14257 frontend.program = program.clone();
14258 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14259 frontend.update_state_after_exec(outcome, true);
14260 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14261 let sketch_id = sketch_object.id;
14262 let sketch = expect_sketch(sketch_object);
14263 let arc1_id = *sketch.segments.get(3).unwrap();
14264 let arc2_id = *sketch.segments.get(7).unwrap();
14265 let axis_id = *sketch.segments.get(10).unwrap();
14266
14267 let constraint = Constraint::Symmetric(Symmetric {
14268 input: vec![arc1_id, arc2_id],
14269 axis: axis_id,
14270 });
14271 let (src_delta, scene_delta) = frontend
14272 .add_constraint(&ctx, version, sketch_id, constraint)
14273 .await
14274 .unwrap();
14275 insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14276 assert_eq!(
14277 scene_delta.new_graph.objects.len(),
14278 14,
14279 "{:#?}",
14280 scene_delta.new_graph.objects
14281 );
14282
14283 ctx.close().await;
14284 }
14285
14286 #[tokio::test(flavor = "multi_thread")]
14287 async fn test_sketch_on_face_simple() {
14288 let initial_source = "\
14289len = 2mm
14290cube = startSketchOn(XY)
14291 |> startProfile(at = [0, 0])
14292 |> line(end = [len, 0], tag = $side)
14293 |> line(end = [0, len])
14294 |> line(end = [-len, 0])
14295 |> line(end = [0, -len])
14296 |> close()
14297 |> extrude(length = len)
14298
14299face = faceOf(cube, face = side)
14300";
14301
14302 let program = Program::parse(initial_source).unwrap().0.unwrap();
14303
14304 let mut frontend = FrontendState::new();
14305
14306 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14307 let mock_ctx = ExecutorContext::new_mock(None).await;
14308 let version = Version(0);
14309
14310 frontend.hack_set_program(&ctx, program).await.unwrap();
14311 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14312 let face_id = face_object.id;
14313
14314 let sketch_args = SketchCtor {
14315 on: Plane::Object(face_id),
14316 };
14317 let (_src_delta, scene_delta, sketch_id) = frontend
14318 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14319 .await
14320 .unwrap();
14321 assert_eq!(sketch_id, ObjectId(2));
14322 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14323 let sketch_object = &scene_delta.new_graph.objects[2];
14324 assert_eq!(sketch_object.id, ObjectId(2));
14325 assert_eq!(
14326 sketch_object.kind,
14327 ObjectKind::Sketch(Sketch {
14328 args: SketchCtor {
14329 on: Plane::Object(face_id),
14330 },
14331 plane: face_id,
14332 segments: vec![],
14333 constraints: vec![],
14334 })
14335 );
14336 assert_eq!(scene_delta.new_graph.objects.len(), 8);
14337
14338 ctx.close().await;
14339 mock_ctx.close().await;
14340 }
14341
14342 #[tokio::test(flavor = "multi_thread")]
14343 async fn test_new_sketch_on_primitive_index_face() {
14344 let initial_source = "\
14345@settings(kclVersion = 2.0)
14346
14347sketch001 = sketch(on = XY) {
14348 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14349}
14350extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14351shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14352 let program = Program::parse(initial_source).unwrap().0.unwrap();
14353 let ctx = ExecutorContext::new_mock(None).await;
14354 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14355 let solid_id = match outcome.variables.get("shell001") {
14356 Some(KclValueView::Solid { value }) => value.id,
14357 value => panic!("expected shell001 to be a solid, got {value:?}"),
14358 };
14359 let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14360
14361 let mut ast = program.ast;
14362 let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14363 let face_expr = sketch_on_ast_expr(
14364 &mut ast,
14365 &scene_graph,
14366 &solid_references,
14367 &Plane::PrimitiveFace(crate::frontend::api::PrimitiveFacePlane { solid_id, index: 6 }),
14368 )
14369 .unwrap();
14370 let face_decl = ast::VariableDeclaration::new(
14371 ast::VariableDeclarator::new("face001", face_expr),
14372 ast::ItemVisibility::Default,
14373 ast::VariableKind::Const,
14374 );
14375 ast.body
14376 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14377 face_decl,
14378 ))));
14379 let face_source = source_from_ast(&ast);
14380 let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14381 insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14382
14383 let program = Program::parse(&new_source).unwrap().0.unwrap();
14384 ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14385 ctx.close().await;
14386 }
14387
14388 #[tokio::test(flavor = "multi_thread")]
14389 async fn test_sketch_on_wall_artifact_from_region_extrude() {
14390 let initial_source = "\
14391s = sketch(on = YZ) {
14392 line1 = line(start = [0, 0], end = [0, 1])
14393 line2 = line(start = [0, 1], end = [1, 1])
14394 line3 = line(start = [1, 1], end = [0, 0])
14395}
14396region001 = region(point = [0.1, 0.1], sketch = s)
14397extrude001 = extrude(region001, length = 5)
14398";
14399
14400 let program = Program::parse(initial_source).unwrap().0.unwrap();
14401
14402 let mut frontend = FrontendState::new();
14403 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14404 let version = Version(0);
14405
14406 frontend.hack_set_program(&ctx, program).await.unwrap();
14407 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14408
14409 let sketch_args = SketchCtor {
14410 on: Plane::Object(wall_object_id),
14411 };
14412 let (src_delta, _scene_delta, _sketch_id) = frontend
14413 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14414 .await
14415 .unwrap();
14416 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14417
14418 ctx.close().await;
14419 }
14420
14421 #[tokio::test(flavor = "multi_thread")]
14422 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14423 let initial_source = "\
14424sketch001 = sketch(on = YZ) {
14425 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14426 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14427 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14428 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14429 coincident([line1.end, line2.start])
14430 coincident([line2.end, line3.start])
14431 coincident([line3.end, line4.start])
14432 coincident([line4.end, line1.start])
14433 parallel([line2, line4])
14434 parallel([line3, line1])
14435 perpendicular([line1, line2])
14436 horizontal(line3)
14437 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14438}
14439region001 = region(point = [3.1, 3.74], sketch = sketch001)
14440extrude001 = extrude(region001, length = 5)
14441";
14442
14443 let program = Program::parse(initial_source).unwrap().0.unwrap();
14444
14445 let mut frontend = FrontendState::new();
14446 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14447 let version = Version(0);
14448
14449 frontend.hack_set_program(&ctx, program).await.unwrap();
14450 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14451
14452 let sketch_args = SketchCtor {
14453 on: Plane::Object(wall_object_id),
14454 };
14455 let (src_delta, _scene_delta, _sketch_id) = frontend
14456 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14457 .await
14458 .unwrap();
14459 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14460
14461 ctx.close().await;
14462 }
14463
14464 #[tokio::test(flavor = "multi_thread")]
14465 async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14466 let initial_source = "\
14467@settings(kclVersion = 2.0)
14468
14469sketch001 = sketch(on = XY) {
14470 circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14471 circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14472}
14473hidden001 = hide(sketch001)
14474region001 = region(segments = [sketch001.circle2])
14475region002 = region(segments = [sketch001.circle1])
14476extrude001 = extrude([region001, region002], length = 5)
14477";
14478
14479 let program = Program::parse(initial_source).unwrap().0.unwrap();
14480 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14481 let version = Version(0);
14482
14483 for (solid_output_index, expected_face) in [
14484 (0, "faceOf(extrude001[0], face = END)"),
14485 (1, "faceOf(extrude001[1], face = END)"),
14486 ] {
14487 let mut frontend = FrontendState::new();
14488 frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14489 let cap_object_id = find_cap_object_id_with_solid_output_index(
14490 &frontend.scene_graph,
14491 crate::frontend::api::CapKind::End,
14492 solid_output_index,
14493 )
14494 .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14495
14496 let sketch_args = SketchCtor {
14497 on: Plane::Object(cap_object_id),
14498 };
14499 let (src_delta, _scene_delta, _sketch_id) = frontend
14500 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14501 .await
14502 .unwrap();
14503
14504 assert!(
14505 src_delta.text.contains(expected_face),
14506 "expected `{expected_face}` in:\n{}",
14507 src_delta.text
14508 );
14509 assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14510 }
14511
14512 ctx.close().await;
14513 }
14514
14515 #[tokio::test(flavor = "multi_thread")]
14516 async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14517 let initial_source = "\
14518@settings(kclVersion = 2.0)
14519
14520sketch001 = sketch(on = XY) {
14521 rect1Line1 = line(start = [0, 0], end = [1, 0])
14522 rect1Line2 = line(start = [1, 0], end = [1, 1])
14523 rect1Line3 = line(start = [1, 1], end = [0, 1])
14524 rect1Line4 = line(start = [0, 1], end = [0, 0])
14525 rect2Line1 = line(start = [3, 0], end = [4, 0])
14526 rect2Line2 = line(start = [4, 0], end = [4, 1])
14527 rect2Line3 = line(start = [4, 1], end = [3, 1])
14528 rect2Line4 = line(start = [3, 1], end = [3, 0])
14529}
14530hidden001 = hide(sketch001)
14531region001 = region(segments = [
14532 sketch001.rect1Line4,
14533 sketch001.rect1Line1
14534])
14535region002 = region(segments = [
14536 sketch001.rect2Line4,
14537 sketch001.rect2Line1
14538])
14539extrude001 = extrude([region001, region002], length = 5)
14540";
14541
14542 let program = Program::parse(initial_source).unwrap().0.unwrap();
14543 let mut frontend = FrontendState::new();
14544 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14545 let version = Version(0);
14546
14547 frontend.hack_set_program(&ctx, program).await.unwrap();
14548 let region_call = "\
14549region(segments = [
14550 sketch001.rect1Line4,
14551 sketch001.rect1Line1
14552])";
14553 let region_call_start = initial_source.find(region_call).unwrap();
14554 let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14555 let segment_call = "line(start = [0, 0], end = [1, 0])";
14556 let segment_call_start = initial_source.find(segment_call).unwrap();
14557 let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14558 let wall_object_id = frontend
14559 .scene_graph
14560 .objects
14561 .iter()
14562 .find_map(|object| match &object.kind {
14563 ObjectKind::Wall(wall)
14564 if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14565 && wall.source.segment.range == segment_range =>
14566 {
14567 Some(object.id)
14568 }
14569 _ => None,
14570 })
14571 .expect("expected a wall object for region001.tags.rect1Line1");
14572
14573 let sketch_args = SketchCtor {
14574 on: Plane::Object(wall_object_id),
14575 };
14576 let (src_delta, _scene_delta, _sketch_id) = frontend
14577 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14578 .await
14579 .unwrap();
14580
14581 let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14582 assert!(
14583 src_delta.text.contains(expected_face),
14584 "expected `{expected_face}` in:\n{}",
14585 src_delta.text
14586 );
14587 assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14588
14589 ctx.close().await;
14590 }
14591
14592 #[test]
14593 fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14594 let source = "\
14595sketch001 = sketch(on = XY) {
14596 line(start = [0, 0], end = [1, 0])
14597}
14598part = subtract(boxSolid, tools = [cutSolid])
14599 |> appearance(color = \"#8f96a3\")
14600";
14601 let ast = Program::parse(source).unwrap().0.unwrap().ast;
14602 let line_start = source.find("line").unwrap();
14603 let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14604 let line_ref = SourceRef::Simple {
14605 range: [line_start, line_end, 0].into(),
14606 node_path: None,
14607 };
14608 assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14609
14610 let subtract_start = source.find("subtract").unwrap();
14611 let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14612 let subtract_ref = SourceRef::Simple {
14613 range: [subtract_start, subtract_end, 0].into(),
14614 node_path: None,
14615 };
14616 assert_eq!(
14617 variable_name_containing_source_ref(&ast, &subtract_ref),
14618 Some("part".to_owned())
14619 );
14620 }
14621
14622 #[tokio::test(flavor = "multi_thread")]
14623 async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14624 clear_mem_cache().await;
14625 let source = "\
14626boxSolid = startSketchOn(XY)
14627 |> startProfile(at = [0, 0])
14628 |> line(end = [4, 0], tag = $bottomEdge)
14629 |> line(end = [0, 4])
14630 |> line(end = [-4, 0])
14631 |> close()
14632 |> extrude(length = 10)
14633cutSolid = startSketchOn(XY)
14634 |> startProfile(at = [1, 1])
14635 |> line(end = [1, 0])
14636 |> line(end = [0, 1])
14637 |> line(end = [-1, 0])
14638 |> close()
14639 |> extrude(length = 10)
14640part = subtract(boxSolid, tools = [cutSolid])
14641 |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14642";
14643 let program = Program::parse(source).unwrap().0.unwrap();
14644 let mut frontend = FrontendState::new();
14645 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14646 match frontend.hack_set_program(&ctx, program).await.unwrap() {
14647 SetProgramOutcome::Success { .. } => {}
14648 SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14649 }
14650
14651 let sweep_call_start = source.find("extrude").unwrap();
14652 let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14653 let part_call_start = source.find("subtract").unwrap();
14654 let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14655 let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14656 let composite_range = [part_call_start, part_call_end, 0].into();
14657
14658 let cap_object = frontend
14659 .scene_graph
14660 .objects
14661 .iter()
14662 .find(|object| {
14663 matches!(
14664 &object.kind,
14665 ObjectKind::Cap(crate::frontend::api::Cap {
14666 kind: crate::frontend::api::CapKind::End,
14667 source,
14668 ..
14669 }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14670 )
14671 })
14672 .expect("expected end cap object to trace through subtract and original extrude");
14673
14674 let mut ast = frontend.program.ast.clone();
14675 let cap_expr = sketch_on_ast_expr(
14676 &mut ast,
14677 &frontend.scene_graph,
14678 &frontend.solid_references,
14679 &Plane::Object(cap_object.id),
14680 )
14681 .unwrap();
14682 let cap_face_decl = ast::VariableDeclaration::new(
14683 ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14684 ast::ItemVisibility::Default,
14685 ast::VariableKind::Const,
14686 );
14687 ast.body
14688 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14689 cap_face_decl,
14690 ))));
14691 let generated_source = source_from_ast(&ast);
14692
14693 assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14694 assert!(!generated_source.contains("faceOf(boxSolid"));
14695 let ast::Expr::CallExpressionKw(call) = cap_expr else {
14696 panic!("expected faceOf call");
14697 };
14698 assert_eq!(call.callee.name.name, "faceOf");
14699 let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14700 panic!("expected solid name");
14701 };
14702 assert_eq!(solid_name.name.name, "part");
14703 let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14704 panic!("expected face name");
14705 };
14706 assert_eq!(face_name.name.name, "END");
14707
14708 ctx.close().await;
14709 }
14710
14711 #[tokio::test(flavor = "multi_thread")]
14712 async fn test_sketch_on_plane_incremental() {
14713 let initial_source = "\
14714len = 2mm
14715cube = startSketchOn(XY)
14716 |> startProfile(at = [0, 0])
14717 |> line(end = [len, 0], tag = $side)
14718 |> line(end = [0, len])
14719 |> line(end = [-len, 0])
14720 |> line(end = [0, -len])
14721 |> close()
14722 |> extrude(length = len)
14723
14724plane = planeOf(cube, face = side)
14725";
14726
14727 let program = Program::parse(initial_source).unwrap().0.unwrap();
14728
14729 let mut frontend = FrontendState::new();
14730
14731 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14732 let mock_ctx = ExecutorContext::new_mock(None).await;
14733 let version = Version(0);
14734
14735 frontend.hack_set_program(&ctx, program).await.unwrap();
14736 let plane_object = frontend
14738 .scene_graph
14739 .objects
14740 .iter()
14741 .rev()
14742 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14743 .unwrap();
14744 let plane_id = plane_object.id;
14745
14746 let sketch_args = SketchCtor {
14747 on: Plane::Object(plane_id),
14748 };
14749 let (src_delta, scene_delta, sketch_id) = frontend
14750 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14751 .await
14752 .unwrap();
14753 insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14754 assert_eq!(sketch_id, ObjectId(2));
14755 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14756 let sketch_object = &scene_delta.new_graph.objects[2];
14757 assert_eq!(sketch_object.id, ObjectId(2));
14758 assert_eq!(
14759 sketch_object.kind,
14760 ObjectKind::Sketch(Sketch {
14761 args: SketchCtor {
14762 on: Plane::Object(plane_id),
14763 },
14764 plane: plane_id,
14765 segments: vec![],
14766 constraints: vec![],
14767 })
14768 );
14769 assert_eq!(scene_delta.new_graph.objects.len(), 9);
14770
14771 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14772 assert_eq!(plane_object.id, plane_id);
14773 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14774
14775 ctx.close().await;
14776 mock_ctx.close().await;
14777 }
14778
14779 #[tokio::test(flavor = "multi_thread")]
14780 async fn test_new_sketch_uses_unique_variable_name() {
14781 let initial_source = "\
14782sketch1 = sketch(on = XY) {
14783}
14784";
14785
14786 let program = Program::parse(initial_source).unwrap().0.unwrap();
14787
14788 let mut frontend = FrontendState::new();
14789 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14790 let version = Version(0);
14791
14792 frontend.hack_set_program(&ctx, program).await.unwrap();
14793
14794 let sketch_args = SketchCtor {
14795 on: Plane::Default(PlaneName::Yz),
14796 };
14797 let (src_delta, _, _) = frontend
14798 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14799 .await
14800 .unwrap();
14801
14802 insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14803
14804 ctx.close().await;
14805 }
14806
14807 #[tokio::test(flavor = "multi_thread")]
14808 async fn test_new_sketch_twice_using_same_plane() {
14809 let initial_source = "\
14810sketch1 = sketch(on = XY) {
14811}
14812";
14813
14814 let program = Program::parse(initial_source).unwrap().0.unwrap();
14815
14816 let mut frontend = FrontendState::new();
14817 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14818 let version = Version(0);
14819
14820 frontend.hack_set_program(&ctx, program).await.unwrap();
14821
14822 let sketch_args = SketchCtor {
14823 on: Plane::Default(PlaneName::Xy),
14824 };
14825 let (src_delta, _, _) = frontend
14826 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14827 .await
14828 .unwrap();
14829
14830 insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14831
14832 ctx.close().await;
14833 }
14834
14835 #[tokio::test(flavor = "multi_thread")]
14836 async fn test_sketch_mode_reuses_cached_on_expression() {
14837 let initial_source = "\
14838width = 2mm
14839sketch(on = offsetPlane(XY, offset = width)) {
14840 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14841 distance([line1.start, line1.end]) == width
14842}
14843";
14844 let program = Program::parse(initial_source).unwrap().0.unwrap();
14845
14846 let mut frontend = FrontendState::new();
14847 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14848 let mock_ctx = ExecutorContext::new_mock(None).await;
14849 let version = Version(0);
14850 let project_id = ProjectId(0);
14851 let file_id = FileId(0);
14852
14853 frontend.hack_set_program(&ctx, program).await.unwrap();
14854 let initial_object_count = frontend.scene_graph.objects.len();
14855 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14856 .expect("Expected sketch object to exist")
14857 .id;
14858
14859 let scene_delta = frontend
14862 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14863 .await
14864 .unwrap();
14865 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14866
14867 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14870 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14871
14872 ctx.close().await;
14873 mock_ctx.close().await;
14874 }
14875
14876 #[tokio::test(flavor = "multi_thread")]
14877 async fn test_edit_sketch_nested_in_pipe() {
14878 clear_mem_cache().await;
14879 let source = r#"
14880profile = sketch(on = XY) {
14881 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14882}
14883 |> translate(x = 2mm)
14884"#;
14885 let program = Program::parse_no_errs(source).unwrap();
14886 let mut frontend = FrontendState::new();
14887 let mock_ctx = ExecutorContext::new_mock(None).await;
14888 let version = Version(0);
14889
14890 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14891 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14892 .expect("Expected piped sketch object")
14893 .id;
14894
14895 let scene_delta = frontend
14896 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14897 .await
14898 .unwrap();
14899 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14900 assert!(
14901 scene_delta
14902 .new_graph
14903 .objects
14904 .iter()
14905 .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14906 "Expected the piped sketch's segments to be present in sketch mode"
14907 );
14908
14909 clear_mem_cache().await;
14910 mock_ctx.close().await;
14911 }
14912
14913 #[tokio::test(flavor = "multi_thread")]
14914 async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14915 clear_mem_cache().await;
14916 let source = r#"
14917useFirstProfile = true
14918
14919profile = if useFirstProfile {
14920 sketch(on = XY) {
14921 line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14922 }
14923} else {
14924 sketch(on = XY) {
14925 line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14926 }
14927}
14928"#;
14929 let program = Program::parse_no_errs(source).unwrap();
14930 let mut frontend = FrontendState::new();
14931 let mock_ctx = ExecutorContext::new_mock(None).await;
14932 let version = Version(0);
14933
14934 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14935 let sketch_object =
14936 find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14937 let sketch_id = sketch_object.id;
14938 let sketch = expect_sketch(sketch_object);
14939 let line_end_id = *sketch
14940 .segments
14941 .get(1)
14942 .expect("Expected the active branch's line end point");
14943
14944 let scene_delta = frontend
14945 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14946 .await
14947 .unwrap();
14948 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14949
14950 let segments = vec![ExistingSegmentCtor {
14951 id: line_end_id,
14952 ctor: SegmentCtor::Point(PointCtor {
14953 position: Point2d {
14954 x: Expr::Var(Number {
14955 value: 30.0,
14956 units: NumericSuffix::Mm,
14957 }),
14958 y: Expr::Var(Number {
14959 value: 15.0,
14960 units: NumericSuffix::Mm,
14961 }),
14962 },
14963 }),
14964 }];
14965 let (source_delta, _) = frontend
14966 .edit_segments(&mock_ctx, version, sketch_id, segments)
14967 .await
14968 .unwrap();
14969 assert!(
14970 source_delta
14971 .text
14972 .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14973 "Expected the active branch's dragged variables to be updated:\n{}",
14974 source_delta.text
14975 );
14976 assert!(
14977 source_delta
14978 .text
14979 .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14980 "Expected the inactive branch to remain unchanged:\n{}",
14981 source_delta.text
14982 );
14983
14984 clear_mem_cache().await;
14985 mock_ctx.close().await;
14986 }
14987
14988 #[tokio::test(flavor = "multi_thread")]
14989 async fn test_multiple_sketch_blocks() {
14990 let initial_source = "\
14991// Cube that requires the engine.
14992width = 2
14993sketch001 = startSketchOn(XY)
14994profile001 = startProfile(sketch001, at = [0, 0])
14995 |> yLine(length = width, tag = $seg1)
14996 |> xLine(length = width)
14997 |> yLine(length = -width)
14998 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14999 |> close()
15000extrude001 = extrude(profile001, length = width)
15001
15002// Get a value that requires the engine.
15003x = segLen(seg1)
15004
15005// Triangle with side length 2*x.
15006sketch(on = XY) {
15007 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
15008 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
15009 coincident([line1.end, line2.start])
15010 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
15011 coincident([line2.end, line3.start])
15012 coincident([line3.end, line1.start])
15013 equalLength([line3, line1])
15014 equalLength([line1, line2])
15015 distance([line1.start, line1.end]) == 2*x
15016}
15017
15018// Line segment with length x.
15019sketch2 = sketch(on = XY) {
15020 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
15021 distance([line1.start, line1.end]) == x
15022}
15023";
15024
15025 let program = Program::parse(initial_source).unwrap().0.unwrap();
15026
15027 let mut frontend = FrontendState::new();
15028
15029 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15030 let mock_ctx = ExecutorContext::new_mock(None).await;
15031 let version = Version(0);
15032 let project_id = ProjectId(0);
15033 let file_id = FileId(0);
15034
15035 frontend.hack_set_program(&ctx, program).await.unwrap();
15036 let sketch_objects = frontend
15037 .scene_graph
15038 .objects
15039 .iter()
15040 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
15041 .collect::<Vec<_>>();
15042 let sketch1_id = sketch_objects.first().unwrap().id;
15043 let sketch2_id = sketch_objects.get(1).unwrap().id;
15044 let point1_id = ObjectId(sketch1_id.0 + 1);
15046 let point2_id = ObjectId(sketch2_id.0 + 1);
15048
15049 let scene_delta = frontend
15058 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15059 .await
15060 .unwrap();
15061 assert_eq!(
15062 scene_delta.new_graph.objects.len(),
15063 18,
15064 "{:#?}",
15065 scene_delta.new_graph.objects
15066 );
15067
15068 let point_ctor = PointCtor {
15070 position: Point2d {
15071 x: Expr::Var(Number {
15072 value: 1.0,
15073 units: NumericSuffix::Mm,
15074 }),
15075 y: Expr::Var(Number {
15076 value: 2.0,
15077 units: NumericSuffix::Mm,
15078 }),
15079 },
15080 };
15081 let segments = vec![ExistingSegmentCtor {
15082 id: point1_id,
15083 ctor: SegmentCtor::Point(point_ctor),
15084 }];
15085 let (src_delta, _) = frontend
15086 .edit_segments(&mock_ctx, version, sketch1_id, segments)
15087 .await
15088 .unwrap();
15089 insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15091 let edited_sketch1_source = src_delta.text.clone();
15092
15093 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15095 assert_eq!(src_delta.text, edited_sketch1_source);
15096 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15104 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15105
15106 let scene_delta = frontend
15114 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15115 .await
15116 .unwrap();
15117 assert_eq!(
15118 scene_delta.new_graph.objects.len(),
15119 24,
15120 "{:#?}",
15121 scene_delta.new_graph.objects
15122 );
15123
15124 let point_ctor = PointCtor {
15126 position: Point2d {
15127 x: Expr::Var(Number {
15128 value: 3.0,
15129 units: NumericSuffix::Mm,
15130 }),
15131 y: Expr::Var(Number {
15132 value: 4.0,
15133 units: NumericSuffix::Mm,
15134 }),
15135 },
15136 };
15137 let segments = vec![ExistingSegmentCtor {
15138 id: point2_id,
15139 ctor: SegmentCtor::Point(point_ctor),
15140 }];
15141 let (src_delta, _) = frontend
15142 .edit_segments(&mock_ctx, version, sketch2_id, segments)
15143 .await
15144 .unwrap();
15145 insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15147 let edited_sketch2_source = src_delta.text.clone();
15148
15149 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15151 assert_eq!(src_delta.text, edited_sketch2_source);
15152
15153 ctx.close().await;
15154 mock_ctx.close().await;
15155 }
15156
15157 #[tokio::test(flavor = "multi_thread")]
15158 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15159 clear_mem_cache().await;
15160
15161 let source = r#"sketch001 = sketch(on = XZ) {
15162 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15163}
15164sketch002 = sketch(on = XY) {
15165 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15166 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15167 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15168 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15169 coincident([line1.end, line2.start])
15170 coincident([line2.end, line3.start])
15171 coincident([line3.end, line4.start])
15172 coincident([line4.end, line1.start])
15173 parallel([line2, line4])
15174 parallel([line3, line1])
15175 perpendicular([line1, line2])
15176 horizontal(line3)
15177 coincident([line1.start, ORIGIN])
15178}
15179"#;
15180
15181 let program = Program::parse(source).unwrap().0.unwrap();
15182 let mut frontend = FrontendState::new();
15183 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15184 let mock_ctx = ExecutorContext::new_mock(None).await;
15185 let version = Version(0);
15186 let project_id = ProjectId(0);
15187 let file_id = FileId(0);
15188
15189 frontend.hack_set_program(&ctx, program).await.unwrap();
15190 let sketch_objects = frontend
15191 .scene_graph
15192 .objects
15193 .iter()
15194 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15195 .collect::<Vec<_>>();
15196 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15197
15198 let sketch1_id = sketch_objects[0].id;
15199 let sketch2_id = sketch_objects[1].id;
15200
15201 frontend
15202 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15203 .await
15204 .unwrap();
15205 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15206
15207 let scene_delta = frontend
15208 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15209 .await
15210 .unwrap();
15211 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15212
15213 clear_mem_cache().await;
15214 ctx.close().await;
15215 mock_ctx.close().await;
15216 }
15217
15218 #[tokio::test(flavor = "multi_thread")]
15223 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15224 let initial_source = "@settings(defaultLengthUnit = mm)
15226
15227sketch001 = sketch(on = XY) {
15228 point(at = [1in, 2in])
15229}
15230";
15231
15232 let program = Program::parse(initial_source).unwrap().0.unwrap();
15233 let mut frontend = FrontendState::new();
15234
15235 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15236 let mock_ctx = ExecutorContext::new_mock(None).await;
15237 let version = Version(0);
15238 let project_id = ProjectId(0);
15239 let file_id = FileId(0);
15240
15241 frontend.hack_set_program(&ctx, program).await.unwrap();
15242 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15243 let sketch_id = sketch_object.id;
15244
15245 frontend
15247 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15248 .await
15249 .unwrap();
15250
15251 let point_ctor = PointCtor {
15253 position: Point2d {
15254 x: Expr::Number(Number {
15255 value: 5.0,
15256 units: NumericSuffix::Mm,
15257 }),
15258 y: Expr::Number(Number {
15259 value: 6.0,
15260 units: NumericSuffix::Mm,
15261 }),
15262 },
15263 };
15264 let segment = SegmentCtor::Point(point_ctor);
15265 let (src_delta, scene_delta) = frontend
15266 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15267 .await
15268 .unwrap();
15269 assert!(
15271 src_delta.text.contains("point(at = [5mm, 6mm])"),
15272 "Expected new point in source, got: {}",
15273 src_delta.text
15274 );
15275 assert!(!scene_delta.new_objects.is_empty());
15276
15277 ctx.close().await;
15278 mock_ctx.close().await;
15279 }
15280
15281 #[tokio::test(flavor = "multi_thread")]
15282 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15283 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15284
15285 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15286 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15287
15288 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15289 assert!(
15290 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15291 "Expected experimental settings to be added to source"
15292 );
15293 }
15294
15295 #[tokio::test(flavor = "multi_thread")]
15296 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15297 let initial_source = "@settings(defaultLengthUnit = mm)
15299
15300s = sketch(on = XY) {}
15301";
15302
15303 let program = Program::parse(initial_source).unwrap().0.unwrap();
15304 let mut frontend = FrontendState::new();
15305
15306 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15307 let mock_ctx = ExecutorContext::new_mock(None).await;
15308 let version = Version(0);
15309
15310 frontend.hack_set_program(&ctx, program).await.unwrap();
15311 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15312 let sketch_id = sketch_object.id;
15313
15314 let line_ctor = LineCtor {
15315 start: Point2d {
15316 x: Expr::Number(Number {
15317 value: 0.0,
15318 units: NumericSuffix::Mm,
15319 }),
15320 y: Expr::Number(Number {
15321 value: 0.0,
15322 units: NumericSuffix::Mm,
15323 }),
15324 },
15325 end: Point2d {
15326 x: Expr::Number(Number {
15327 value: 10.0,
15328 units: NumericSuffix::Mm,
15329 }),
15330 y: Expr::Number(Number {
15331 value: 10.0,
15332 units: NumericSuffix::Mm,
15333 }),
15334 },
15335 construction: None,
15336 };
15337 let segment = SegmentCtor::Line(line_ctor);
15338 let (src_delta, scene_delta) = frontend
15339 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15340 .await
15341 .unwrap();
15342 assert!(
15343 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15344 "Expected line in source, got: {}",
15345 src_delta.text
15346 );
15347 assert_eq!(scene_delta.new_objects.len(), 3);
15349
15350 ctx.close().await;
15351 mock_ctx.close().await;
15352 }
15353
15354 #[tokio::test(flavor = "multi_thread")]
15355 async fn test_extra_newlines_between_operations_edit_line() {
15356 let initial_source = "@settings(defaultLengthUnit = mm)
15358
15359sketch001 = sketch(on = XY) {
15360
15361 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15362
15363}
15364";
15365
15366 let program = Program::parse(initial_source).unwrap().0.unwrap();
15367 let mut frontend = FrontendState::new();
15368
15369 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15370 let mock_ctx = ExecutorContext::new_mock(None).await;
15371 let version = Version(0);
15372 let project_id = ProjectId(0);
15373 let file_id = FileId(0);
15374
15375 frontend.hack_set_program(&ctx, program).await.unwrap();
15376 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15377 let sketch_id = sketch_object.id;
15378 let sketch = expect_sketch(sketch_object);
15379
15380 let line_id = sketch
15382 .segments
15383 .iter()
15384 .copied()
15385 .find(|seg_id| {
15386 matches!(
15387 &frontend.scene_graph.objects[seg_id.0].kind,
15388 ObjectKind::Segment {
15389 segment: Segment::Line(_)
15390 }
15391 )
15392 })
15393 .expect("Expected a line segment in sketch");
15394
15395 frontend
15397 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15398 .await
15399 .unwrap();
15400
15401 let line_ctor = LineCtor {
15403 start: Point2d {
15404 x: Expr::Var(Number {
15405 value: 1.0,
15406 units: NumericSuffix::Mm,
15407 }),
15408 y: Expr::Var(Number {
15409 value: 2.0,
15410 units: NumericSuffix::Mm,
15411 }),
15412 },
15413 end: Point2d {
15414 x: Expr::Var(Number {
15415 value: 13.0,
15416 units: NumericSuffix::Mm,
15417 }),
15418 y: Expr::Var(Number {
15419 value: 14.0,
15420 units: NumericSuffix::Mm,
15421 }),
15422 },
15423 construction: None,
15424 };
15425 let segments = vec![ExistingSegmentCtor {
15426 id: line_id,
15427 ctor: SegmentCtor::Line(line_ctor),
15428 }];
15429 let (src_delta, _scene_delta) = frontend
15430 .edit_segments(&mock_ctx, version, sketch_id, segments)
15431 .await
15432 .unwrap();
15433 assert!(
15434 src_delta
15435 .text
15436 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15437 "Expected edited line in source, got: {}",
15438 src_delta.text
15439 );
15440
15441 ctx.close().await;
15442 mock_ctx.close().await;
15443 }
15444
15445 #[tokio::test(flavor = "multi_thread")]
15446 async fn test_extra_newlines_delete_segment() {
15447 let initial_source = "@settings(defaultLengthUnit = mm)
15449
15450sketch001 = sketch(on = XY) {
15451 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15452}
15453";
15454
15455 let program = Program::parse(initial_source).unwrap().0.unwrap();
15456 let mut frontend = FrontendState::new();
15457
15458 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15459 let mock_ctx = ExecutorContext::new_mock(None).await;
15460 let version = Version(0);
15461
15462 frontend.hack_set_program(&ctx, program).await.unwrap();
15463 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15464 let sketch_id = sketch_object.id;
15465 let sketch = expect_sketch(sketch_object);
15466
15467 assert_eq!(sketch.segments.len(), 3);
15469 let circle_id = sketch.segments[2];
15470
15471 let (src_delta, scene_delta) = frontend
15473 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15474 .await
15475 .unwrap();
15476 assert!(
15477 src_delta.text.contains("sketch(on = XY) {"),
15478 "Expected sketch block in source, got: {}",
15479 src_delta.text
15480 );
15481 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15482 let new_sketch = expect_sketch(new_sketch_object);
15483 assert_eq!(new_sketch.segments.len(), 0);
15484
15485 ctx.close().await;
15486 mock_ctx.close().await;
15487 }
15488
15489 #[tokio::test(flavor = "multi_thread")]
15490 async fn test_unformatted_source_add_arc() {
15491 let initial_source = "@settings(defaultLengthUnit = mm)
15493
15494sketch001 = sketch(on = XY) {
15495}
15496";
15497
15498 let program = Program::parse(initial_source).unwrap().0.unwrap();
15499 let mut frontend = FrontendState::new();
15500
15501 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15502 let mock_ctx = ExecutorContext::new_mock(None).await;
15503 let version = Version(0);
15504
15505 frontend.hack_set_program(&ctx, program).await.unwrap();
15506 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15507 let sketch_id = sketch_object.id;
15508
15509 let arc_ctor = ArcCtor {
15510 start: Point2d {
15511 x: Expr::Var(Number {
15512 value: 5.0,
15513 units: NumericSuffix::Mm,
15514 }),
15515 y: Expr::Var(Number {
15516 value: 0.0,
15517 units: NumericSuffix::Mm,
15518 }),
15519 },
15520 end: Point2d {
15521 x: Expr::Var(Number {
15522 value: 0.0,
15523 units: NumericSuffix::Mm,
15524 }),
15525 y: Expr::Var(Number {
15526 value: 5.0,
15527 units: NumericSuffix::Mm,
15528 }),
15529 },
15530 center: Point2d {
15531 x: Expr::Var(Number {
15532 value: 0.0,
15533 units: NumericSuffix::Mm,
15534 }),
15535 y: Expr::Var(Number {
15536 value: 0.0,
15537 units: NumericSuffix::Mm,
15538 }),
15539 },
15540 direction: None,
15541 construction: None,
15542 };
15543 let segment = SegmentCtor::Arc(arc_ctor);
15544 let (src_delta, scene_delta) = frontend
15545 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15546 .await
15547 .unwrap();
15548 assert!(
15549 src_delta
15550 .text
15551 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15552 "Expected arc in source, got: {}",
15553 src_delta.text
15554 );
15555 assert!(!scene_delta.new_objects.is_empty());
15556
15557 ctx.close().await;
15558 mock_ctx.close().await;
15559 }
15560
15561 #[tokio::test(flavor = "multi_thread")]
15562 async fn test_arc_direction_flows_to_source() {
15563 let initial_source = "@settings(defaultLengthUnit = mm)
15564
15565sketch001 = sketch(on = XY) {
15566}
15567";
15568
15569 let program = Program::parse(initial_source).unwrap().0.unwrap();
15570 let mut frontend = FrontendState::new();
15571
15572 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15573 let mock_ctx = ExecutorContext::new_mock(None).await;
15574 let version = Version(0);
15575
15576 frontend.hack_set_program(&ctx, program).await.unwrap();
15577 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15578 let sketch_id = sketch_object.id;
15579
15580 let point = |x: f64, y: f64| Point2d {
15581 x: Expr::Var(Number {
15582 value: x,
15583 units: NumericSuffix::Mm,
15584 }),
15585 y: Expr::Var(Number {
15586 value: y,
15587 units: NumericSuffix::Mm,
15588 }),
15589 };
15590
15591 let arc_ctor = ArcCtor {
15593 start: point(5.0, 0.0),
15594 end: point(0.0, 5.0),
15595 center: point(0.0, 0.0),
15596 direction: Some(ArcDirection::Cw),
15597 construction: None,
15598 };
15599 let (src_delta, scene_delta) = frontend
15600 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15601 .await
15602 .unwrap();
15603 assert!(
15604 src_delta.text.contains("direction = CW"),
15605 "Expected direction = CW in source, got: {}",
15606 src_delta.text
15607 );
15608 let arc_id = *scene_delta.new_objects.last().unwrap();
15610
15611 let edited_ctor = ArcCtor {
15615 start: point(0.0, -5.0),
15616 end: point(0.0, 5.0),
15617 center: point(0.0, 0.0),
15618 direction: Some(ArcDirection::Cw),
15619 construction: None,
15620 };
15621 let (src_delta, _scene_delta) = frontend
15622 .edit_segments(
15623 &mock_ctx,
15624 version,
15625 sketch_id,
15626 vec![ExistingSegmentCtor {
15627 id: arc_id,
15628 ctor: SegmentCtor::Arc(edited_ctor),
15629 }],
15630 )
15631 .await
15632 .unwrap();
15633 assert!(
15634 src_delta.text.contains("start = [var 0mm, var -5mm]"),
15635 "Expected edited start point in source, got: {}",
15636 src_delta.text
15637 );
15638 assert!(
15639 src_delta.text.contains("direction = CW"),
15640 "Expected direction = CW to be preserved in source, got: {}",
15641 src_delta.text
15642 );
15643
15644 let edited_ctor = ArcCtor {
15647 start: point(0.0, -5.0),
15648 end: point(0.0, 5.0),
15649 center: point(0.0, 0.0),
15650 direction: Some(ArcDirection::Ccw),
15651 construction: None,
15652 };
15653 let (src_delta, _scene_delta) = frontend
15654 .edit_segments(
15655 &mock_ctx,
15656 version,
15657 sketch_id,
15658 vec![ExistingSegmentCtor {
15659 id: arc_id,
15660 ctor: SegmentCtor::Arc(edited_ctor),
15661 }],
15662 )
15663 .await
15664 .unwrap();
15665 assert!(
15666 !src_delta.text.contains("direction"),
15667 "Expected direction argument to be removed from source, got: {}",
15668 src_delta.text
15669 );
15670
15671 ctx.close().await;
15672 mock_ctx.close().await;
15673 }
15674
15675 #[tokio::test(flavor = "multi_thread")]
15676 async fn test_extra_newlines_add_circle() {
15677 let initial_source = "@settings(defaultLengthUnit = mm)
15679
15680sketch001 = sketch(on = XY) {
15681}
15682";
15683
15684 let program = Program::parse(initial_source).unwrap().0.unwrap();
15685 let mut frontend = FrontendState::new();
15686
15687 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15688 let mock_ctx = ExecutorContext::new_mock(None).await;
15689 let version = Version(0);
15690
15691 frontend.hack_set_program(&ctx, program).await.unwrap();
15692 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15693 let sketch_id = sketch_object.id;
15694
15695 let circle_ctor = CircleCtor {
15696 start: Point2d {
15697 x: Expr::Var(Number {
15698 value: 5.0,
15699 units: NumericSuffix::Mm,
15700 }),
15701 y: Expr::Var(Number {
15702 value: 0.0,
15703 units: NumericSuffix::Mm,
15704 }),
15705 },
15706 center: Point2d {
15707 x: Expr::Var(Number {
15708 value: 0.0,
15709 units: NumericSuffix::Mm,
15710 }),
15711 y: Expr::Var(Number {
15712 value: 0.0,
15713 units: NumericSuffix::Mm,
15714 }),
15715 },
15716 construction: None,
15717 };
15718 let segment = SegmentCtor::Circle(circle_ctor);
15719 let (src_delta, scene_delta) = frontend
15720 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15721 .await
15722 .unwrap();
15723 assert!(
15724 src_delta
15725 .text
15726 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15727 "Expected circle in source, got: {}",
15728 src_delta.text
15729 );
15730 assert!(!scene_delta.new_objects.is_empty());
15731
15732 ctx.close().await;
15733 mock_ctx.close().await;
15734 }
15735
15736 #[tokio::test(flavor = "multi_thread")]
15737 async fn test_extra_newlines_add_constraint() {
15738 let initial_source = "@settings(defaultLengthUnit = mm)
15740
15741sketch001 = sketch(on = XY) {
15742 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15743 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15744}
15745";
15746
15747 let program = Program::parse(initial_source).unwrap().0.unwrap();
15748 let mut frontend = FrontendState::new();
15749
15750 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15751 let mock_ctx = ExecutorContext::new_mock(None).await;
15752 let version = Version(0);
15753 let project_id = ProjectId(0);
15754 let file_id = FileId(0);
15755
15756 frontend.hack_set_program(&ctx, program).await.unwrap();
15757 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15758 let sketch_id = sketch_object.id;
15759 let sketch = expect_sketch(sketch_object);
15760
15761 let line_ids: Vec<ObjectId> = sketch
15763 .segments
15764 .iter()
15765 .copied()
15766 .filter(|seg_id| {
15767 matches!(
15768 &frontend.scene_graph.objects[seg_id.0].kind,
15769 ObjectKind::Segment {
15770 segment: Segment::Line(_)
15771 }
15772 )
15773 })
15774 .collect();
15775 assert_eq!(line_ids.len(), 2, "Expected two line segments");
15776
15777 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15778 let ObjectKind::Segment {
15779 segment: Segment::Line(line1_data),
15780 } = &line1.kind
15781 else {
15782 panic!("Expected line");
15783 };
15784 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15785 let ObjectKind::Segment {
15786 segment: Segment::Line(line2_data),
15787 } = &line2.kind
15788 else {
15789 panic!("Expected line");
15790 };
15791
15792 let constraint = Constraint::Coincident(Coincident {
15794 segments: vec![line1_data.end.into(), line2_data.start.into()],
15795 });
15796
15797 frontend
15799 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15800 .await
15801 .unwrap();
15802 let (src_delta, _scene_delta) = frontend
15803 .add_constraint(&mock_ctx, version, sketch_id, constraint)
15804 .await
15805 .unwrap();
15806 assert!(
15807 src_delta.text.contains("coincident("),
15808 "Expected coincident constraint in source, got: {}",
15809 src_delta.text
15810 );
15811
15812 ctx.close().await;
15813 mock_ctx.close().await;
15814 }
15815
15816 #[tokio::test(flavor = "multi_thread")]
15817 async fn test_extra_newlines_add_line_then_edit_line() {
15818 let initial_source = "@settings(defaultLengthUnit = mm)
15820
15821sketch001 = sketch(on = XY) {
15822}
15823";
15824
15825 let program = Program::parse(initial_source).unwrap().0.unwrap();
15826 let mut frontend = FrontendState::new();
15827
15828 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15829 let mock_ctx = ExecutorContext::new_mock(None).await;
15830 let version = Version(0);
15831
15832 frontend.hack_set_program(&ctx, program).await.unwrap();
15833 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15834 let sketch_id = sketch_object.id;
15835
15836 let line_ctor = LineCtor {
15838 start: Point2d {
15839 x: Expr::Number(Number {
15840 value: 0.0,
15841 units: NumericSuffix::Mm,
15842 }),
15843 y: Expr::Number(Number {
15844 value: 0.0,
15845 units: NumericSuffix::Mm,
15846 }),
15847 },
15848 end: Point2d {
15849 x: Expr::Number(Number {
15850 value: 10.0,
15851 units: NumericSuffix::Mm,
15852 }),
15853 y: Expr::Number(Number {
15854 value: 10.0,
15855 units: NumericSuffix::Mm,
15856 }),
15857 },
15858 construction: None,
15859 };
15860 let segment = SegmentCtor::Line(line_ctor);
15861 let (src_delta, scene_delta) = frontend
15862 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15863 .await
15864 .unwrap();
15865 assert!(
15866 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15867 "Expected line in source after add, got: {}",
15868 src_delta.text
15869 );
15870 let line_id = *scene_delta.new_objects.last().unwrap();
15872
15873 let line_ctor = LineCtor {
15875 start: Point2d {
15876 x: Expr::Number(Number {
15877 value: 1.0,
15878 units: NumericSuffix::Mm,
15879 }),
15880 y: Expr::Number(Number {
15881 value: 2.0,
15882 units: NumericSuffix::Mm,
15883 }),
15884 },
15885 end: Point2d {
15886 x: Expr::Number(Number {
15887 value: 13.0,
15888 units: NumericSuffix::Mm,
15889 }),
15890 y: Expr::Number(Number {
15891 value: 14.0,
15892 units: NumericSuffix::Mm,
15893 }),
15894 },
15895 construction: None,
15896 };
15897 let segments = vec![ExistingSegmentCtor {
15898 id: line_id,
15899 ctor: SegmentCtor::Line(line_ctor),
15900 }];
15901 let (src_delta, scene_delta) = frontend
15902 .edit_segments(&mock_ctx, version, sketch_id, segments)
15903 .await
15904 .unwrap();
15905 assert!(
15906 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15907 "Expected edited line in source, got: {}",
15908 src_delta.text
15909 );
15910 assert_eq!(scene_delta.new_objects, vec![]);
15911
15912 ctx.close().await;
15913 mock_ctx.close().await;
15914 }
15915
15916 #[test]
15917 fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15918 let code = "\
15927foo1 = 1
15928sk = sketch() {
15929 p = var 1.5
15930}
159317 + 8
15932";
15933 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15934 let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15935 panic!("expected an expression statement");
15936 };
15937 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15938 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15939 &mut ast,
15940 &source_ref,
15941 AstMutateCommand::AddVariableDeclaration {
15942 prefix: "foo".to_owned(),
15943 },
15944 )
15945 .unwrap();
15946 let AstMutateCommandReturn::Name(name) = cmd_return else {
15947 panic!("expected a generated name");
15948 };
15949 assert_eq!(name, "foo2");
15950 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15951 panic!("expected the expression statement to become a variable declaration");
15952 };
15953 assert_eq!(decl.name(), "foo2");
15954 }
15955
15956 fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15958 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15959 panic!("expected a variable declaration");
15960 };
15961 let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15962 panic!("expected a function expression");
15963 };
15964 &func.body
15965 }
15966
15967 fn then_block_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15970 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15971 panic!("expected a variable declaration");
15972 };
15973 let ast::Expr::IfExpression(if_expr) = &decl.declaration.init else {
15974 panic!("expected an if expression");
15975 };
15976 &if_expr.then_val
15977 }
15978
15979 #[test]
15980 fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15981 let code = "\
15986fn build() {
15987 thing1 = 1
15988 10 + 20
15989 return thing1
15990}
15991";
15992 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15993 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15994 panic!("expected an expression statement");
15995 };
15996 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15997 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15998 &mut ast,
15999 &source_ref,
16000 AstMutateCommand::AddVariableDeclaration {
16001 prefix: "thing".to_owned(),
16002 },
16003 )
16004 .unwrap();
16005 let AstMutateCommandReturn::Name(name) = cmd_return else {
16006 panic!("expected a generated name");
16007 };
16008 assert_eq!(name, "thing2");
16009 let body = &function_body_at(&ast, 0).body;
16010 assert_eq!(body.len(), 3);
16011 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16012 panic!("expected the expression statement to become a variable declaration");
16013 };
16014 assert_eq!(decl.name(), "thing2");
16015 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16017 panic!("expected a variable declaration");
16018 };
16019 assert_eq!(first.name(), "thing1");
16020 assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
16021 }
16022
16023 #[test]
16024 fn test_delete_node_in_function_body_preserves_leading_comment() {
16025 let code = "\
16029fn build() {
16030 a = 1
16031 // keep me
16032 b = 2
16033 return a
16034}
16035";
16036 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16037 let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
16038 panic!("expected a variable declaration");
16039 };
16040 assert_eq!(b_decl.name(), "b");
16041 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16042 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16043 let body = &function_body_at(&ast, 0).body;
16044 assert_eq!(body.len(), 2, "expected b to be deleted");
16045 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16046 panic!("expected a variable declaration");
16047 };
16048 assert_eq!(first.name(), "a");
16049 let ast::BodyItem::ReturnStatement(_) = &body[1] else {
16050 panic!("expected the return statement to remain");
16051 };
16052 assert!(
16053 body[1].get_comments().iter().any(|c| c.contains("keep me")),
16054 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16055 body[1].get_comments()
16056 );
16057 }
16058
16059 #[test]
16060 fn test_add_variable_declaration_in_function_body_ignores_parameters() {
16061 let code = "\
16069fn build(thing2) {
16070 thing1 = 1
16071 10 + 20
16072 return thing1 + thing2
16073}
16074";
16075 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16076 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16077 panic!("expected an expression statement");
16078 };
16079 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16080 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16081 &mut ast,
16082 &source_ref,
16083 AstMutateCommand::AddVariableDeclaration {
16084 prefix: "thing".to_owned(),
16085 },
16086 )
16087 .unwrap();
16088 let AstMutateCommandReturn::Name(name) = cmd_return else {
16089 panic!("expected a generated name");
16090 };
16091 assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16092 }
16093
16094 #[test]
16095 fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16096 let code = "\
16097x = 1
16098y = if x > 0 {
16099 q1 = 1
16100 foo(q1)
16101 q1
16102} else {
16103 2
16104}
16105";
16106 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16107 let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16108 panic!("expected an expression statement");
16109 };
16110 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16111 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16112 &mut ast,
16113 &source_ref,
16114 AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16115 )
16116 .unwrap();
16117 let AstMutateCommandReturn::Name(name) = cmd_return else {
16118 panic!("expected a generated name");
16119 };
16120 assert_eq!(name, "q2");
16121 let body = &then_block_at(&ast, 1).body;
16122 assert_eq!(body.len(), 3);
16123 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16124 panic!("expected the expression statement to become a variable declaration");
16125 };
16126 assert_eq!(decl.name(), "q2");
16127 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16129 panic!("expected a variable declaration");
16130 };
16131 assert_eq!(first.name(), "q1");
16132 assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16133 }
16134
16135 #[test]
16136 fn test_delete_node_in_if_branch_preserves_leading_comment() {
16137 let code = "\
16140y = if true {
16141 a = 1
16142 // keep me
16143 b = 2
16144 a + b
16145} else {
16146 2
16147}
16148";
16149 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16150 let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16151 panic!("expected a variable declaration");
16152 };
16153 assert_eq!(b_decl.name(), "b");
16154 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16155 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16156 let body = &then_block_at(&ast, 0).body;
16157 assert_eq!(body.len(), 2, "expected b to be deleted");
16158 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16159 panic!("expected a variable declaration");
16160 };
16161 assert_eq!(first.name(), "a");
16162 let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16163 panic!("expected the tail expression to remain");
16164 };
16165 assert!(
16166 body[1].get_comments().iter().any(|c| c.contains("keep me")),
16167 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16168 body[1].get_comments()
16169 );
16170 }
16171}