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 default_planes,
2057 ..
2058 } = err;
2059
2060 non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2061
2062 Ok(ExecOutcome {
2063 variables,
2064 filenames,
2065 operations,
2066 artifact_graph,
2067 scene_objects,
2068 source_range_to_object,
2069 var_solutions,
2070 refactor_metadata,
2071 issues: non_fatal,
2072 default_planes,
2073 })
2074 }
2075
2076 async fn add_point(
2077 &mut self,
2078 ctx: &ExecutorContext,
2079 sketch: ObjectId,
2080 ctor: PointCtor,
2081 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2082 let at_ast = to_ast_point2d(&ctor.position)
2084 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2085 let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2086 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2087 unlabeled: None,
2088 arguments: vec![ast::LabeledArg {
2089 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2090 arg: at_ast,
2091 }],
2092 digest: None,
2093 non_code_meta: Default::default(),
2094 })));
2095
2096 let sketch_id = sketch;
2098 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2099 #[cfg(target_arch = "wasm32")]
2100 web_sys::console::error_1(
2101 &format!(
2102 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2103 self.scene_graph.objects
2104 )
2105 .into(),
2106 );
2107 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2108 })?;
2109 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2110 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2111 "Object is not a sketch, it is {}",
2112 sketch_object.kind.human_friendly_kind_with_article(),
2113 ))));
2114 };
2115 let mut new_ast = self.program.ast.clone();
2117 let (sketch_block_ref, _) = self
2118 .mutate_ast(
2119 &mut new_ast,
2120 sketch_id,
2121 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2122 )
2123 .map_err(KclErrorWithOutputs::no_outputs)?;
2124 let new_source = source_from_ast(&new_ast);
2126 let new_program = parse_frontend_mutation_source(
2128 &new_source,
2129 "Error parsing KCL source after adding point",
2130 "No AST produced after adding point",
2131 )?;
2132
2133 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2134 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2135 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2136 )))
2137 })?;
2138
2139 self.program = new_program.clone();
2141
2142 let mut truncated_program = new_program;
2144 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2145 .map_err(KclErrorWithOutputs::no_outputs)?;
2146
2147 let outcome = ctx
2149 .run_mock(
2150 &truncated_program,
2151 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2152 )
2153 .await?;
2154
2155 let new_object_ids = {
2156 let make_err =
2157 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2158 let segment_id = outcome
2159 .source_range_to_object
2160 .get(&point_node_ref.range)
2161 .copied()
2162 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2163 let segment_object = outcome
2164 .scene_objects
2165 .get(segment_id.0)
2166 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2167 let ObjectKind::Segment { segment } = &segment_object.kind else {
2168 return Err(make_err(format!(
2169 "Object is not a segment, it is {}",
2170 segment_object.kind.human_friendly_kind_with_article()
2171 )));
2172 };
2173 let Segment::Point(_) = segment else {
2174 return Err(make_err(format!(
2175 "Segment is not a point, it is {}",
2176 segment.human_friendly_kind_with_article()
2177 )));
2178 };
2179 vec![segment_id]
2180 };
2181 let src_delta = SourceDelta { text: new_source };
2182 let outcome = self.update_state_after_exec(outcome, false);
2184 let scene_graph_delta = SceneGraphDelta {
2185 new_graph: self.scene_graph_for_ui(),
2186 invalidates_ids: false,
2187 new_objects: new_object_ids,
2188 exec_outcome: outcome,
2189 };
2190 Ok((src_delta, scene_graph_delta))
2191 }
2192
2193 async fn add_line(
2194 &mut self,
2195 ctx: &ExecutorContext,
2196 sketch: ObjectId,
2197 ctor: LineCtor,
2198 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2199 let start_ast = to_ast_point2d(&ctor.start)
2201 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2202 let end_ast = to_ast_point2d(&ctor.end)
2203 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2204 let mut arguments = vec![
2205 ast::LabeledArg {
2206 label: Some(ast::Identifier::new(LINE_START_PARAM)),
2207 arg: start_ast,
2208 },
2209 ast::LabeledArg {
2210 label: Some(ast::Identifier::new(LINE_END_PARAM)),
2211 arg: end_ast,
2212 },
2213 ];
2214 if ctor.construction == Some(true) {
2216 arguments.push(ast::LabeledArg {
2217 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2218 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2219 value: ast::LiteralValue::Bool(true),
2220 raw: "true".to_string(),
2221 digest: None,
2222 }))),
2223 });
2224 }
2225 let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2226 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2227 unlabeled: None,
2228 arguments,
2229 digest: None,
2230 non_code_meta: Default::default(),
2231 })));
2232
2233 let sketch_id = sketch;
2235 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2236 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2237 })?;
2238 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2239 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2240 "Object is not a sketch, it is {}",
2241 sketch_object.kind.human_friendly_kind_with_article(),
2242 ))));
2243 };
2244 let mut new_ast = self.program.ast.clone();
2246 let (sketch_block_ref, _) = self
2247 .mutate_ast(
2248 &mut new_ast,
2249 sketch_id,
2250 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2251 )
2252 .map_err(KclErrorWithOutputs::no_outputs)?;
2253 let new_source = source_from_ast(&new_ast);
2255 let new_program = parse_frontend_mutation_source(
2257 &new_source,
2258 "Error parsing KCL source after adding line",
2259 "No AST produced after adding line",
2260 )?;
2261
2262 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2263 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2264 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2265 )))
2266 })?;
2267
2268 self.program = new_program.clone();
2270
2271 let mut truncated_program = new_program;
2273 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2274 .map_err(KclErrorWithOutputs::no_outputs)?;
2275
2276 let outcome = ctx
2278 .run_mock(
2279 &truncated_program,
2280 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2281 )
2282 .await?;
2283
2284 let new_object_ids = {
2285 let make_err =
2286 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2287 let segment_id = outcome
2288 .source_range_to_object
2289 .get(&line_node_ref.range)
2290 .copied()
2291 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2292 let segment_object = outcome
2293 .scene_object_by_id(segment_id)
2294 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2295 let ObjectKind::Segment { segment } = &segment_object.kind else {
2296 return Err(make_err(format!(
2297 "Object is not a segment, it is {}",
2298 segment_object.kind.human_friendly_kind_with_article()
2299 )));
2300 };
2301 let Segment::Line(line) = segment else {
2302 return Err(make_err(format!(
2303 "Segment is not a line, it is {}",
2304 segment.human_friendly_kind_with_article()
2305 )));
2306 };
2307 vec![line.start, line.end, segment_id]
2308 };
2309 let src_delta = SourceDelta { text: new_source };
2310 let outcome = self.update_state_after_exec(outcome, false);
2312 let scene_graph_delta = SceneGraphDelta {
2313 new_graph: self.scene_graph_for_ui(),
2314 invalidates_ids: false,
2315 new_objects: new_object_ids,
2316 exec_outcome: outcome,
2317 };
2318 Ok((src_delta, scene_graph_delta))
2319 }
2320
2321 async fn add_arc(
2322 &mut self,
2323 ctx: &ExecutorContext,
2324 sketch: ObjectId,
2325 ctor: ArcCtor,
2326 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2327 let start_ast = to_ast_point2d(&ctor.start)
2329 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2330 let end_ast = to_ast_point2d(&ctor.end)
2331 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2332 let center_ast = to_ast_point2d(&ctor.center)
2333 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2334 let mut arguments = vec![
2335 ast::LabeledArg {
2336 label: Some(ast::Identifier::new(ARC_START_PARAM)),
2337 arg: start_ast,
2338 },
2339 ast::LabeledArg {
2340 label: Some(ast::Identifier::new(ARC_END_PARAM)),
2341 arg: end_ast,
2342 },
2343 ast::LabeledArg {
2344 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2345 arg: center_ast,
2346 },
2347 ];
2348 if ctor.direction == Some(ArcDirection::Cw) {
2351 arguments.push(ast::LabeledArg {
2352 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2353 arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2354 });
2355 }
2356 if ctor.construction == Some(true) {
2358 arguments.push(ast::LabeledArg {
2359 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2360 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2361 value: ast::LiteralValue::Bool(true),
2362 raw: "true".to_string(),
2363 digest: None,
2364 }))),
2365 });
2366 }
2367 let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2368 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2369 unlabeled: None,
2370 arguments,
2371 digest: None,
2372 non_code_meta: Default::default(),
2373 })));
2374
2375 let sketch_id = sketch;
2377 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2378 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2379 })?;
2380 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2381 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2382 "Object is not a sketch, it is {}",
2383 sketch_object.kind.human_friendly_kind_with_article(),
2384 ))));
2385 };
2386 let mut new_ast = self.program.ast.clone();
2388 let (sketch_block_ref, _) = self
2389 .mutate_ast(
2390 &mut new_ast,
2391 sketch_id,
2392 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2393 )
2394 .map_err(KclErrorWithOutputs::no_outputs)?;
2395 let new_source = source_from_ast(&new_ast);
2397 let new_program = parse_frontend_mutation_source(
2399 &new_source,
2400 "Error parsing KCL source after adding arc",
2401 "No AST produced after adding arc",
2402 )?;
2403
2404 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2405 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2406 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2407 )))
2408 })?;
2409
2410 self.program = new_program.clone();
2412
2413 let mut truncated_program = new_program;
2415 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2416 .map_err(KclErrorWithOutputs::no_outputs)?;
2417
2418 let outcome = ctx
2420 .run_mock(
2421 &truncated_program,
2422 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2423 )
2424 .await?;
2425
2426 let new_object_ids = {
2427 let make_err =
2428 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2429 let segment_id = outcome
2430 .source_range_to_object
2431 .get(&arc_node_ref.range)
2432 .copied()
2433 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2434 let segment_object = outcome
2435 .scene_objects
2436 .get(segment_id.0)
2437 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2438 let ObjectKind::Segment { segment } = &segment_object.kind else {
2439 return Err(make_err(format!(
2440 "Object is not a segment, it is {}",
2441 segment_object.kind.human_friendly_kind_with_article()
2442 )));
2443 };
2444 let Segment::Arc(arc) = segment else {
2445 return Err(make_err(format!(
2446 "Segment is not an arc, it is {}",
2447 segment.human_friendly_kind_with_article()
2448 )));
2449 };
2450 vec![arc.start, arc.end, arc.center, segment_id]
2451 };
2452 let src_delta = SourceDelta { text: new_source };
2453 let outcome = self.update_state_after_exec(outcome, false);
2455 let scene_graph_delta = SceneGraphDelta {
2456 new_graph: self.scene_graph_for_ui(),
2457 invalidates_ids: false,
2458 new_objects: new_object_ids,
2459 exec_outcome: outcome,
2460 };
2461 Ok((src_delta, scene_graph_delta))
2462 }
2463
2464 async fn add_circle(
2465 &mut self,
2466 ctx: &ExecutorContext,
2467 sketch: ObjectId,
2468 ctor: CircleCtor,
2469 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2470 let start_ast = to_ast_point2d(&ctor.start)
2472 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2473 let center_ast = to_ast_point2d(&ctor.center)
2474 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2475 let mut arguments = vec![
2476 ast::LabeledArg {
2477 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2478 arg: start_ast,
2479 },
2480 ast::LabeledArg {
2481 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2482 arg: center_ast,
2483 },
2484 ];
2485 if ctor.construction == Some(true) {
2487 arguments.push(ast::LabeledArg {
2488 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2489 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2490 value: ast::LiteralValue::Bool(true),
2491 raw: "true".to_string(),
2492 digest: None,
2493 }))),
2494 });
2495 }
2496 let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2497 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2498 unlabeled: None,
2499 arguments,
2500 digest: None,
2501 non_code_meta: Default::default(),
2502 })));
2503
2504 let sketch_id = sketch;
2506 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2507 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2508 })?;
2509 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2510 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2511 "Object is not a sketch, it is {}",
2512 sketch_object.kind.human_friendly_kind_with_article(),
2513 ))));
2514 };
2515 let mut new_ast = self.program.ast.clone();
2517 let (sketch_block_ref, _) = self
2518 .mutate_ast(
2519 &mut new_ast,
2520 sketch_id,
2521 AstMutateCommand::AddSketchBlockVarDecl {
2522 prefix: CIRCLE_VARIABLE.to_owned(),
2523 expr: circle_ast,
2524 },
2525 )
2526 .map_err(KclErrorWithOutputs::no_outputs)?;
2527 let new_source = source_from_ast(&new_ast);
2529 let new_program = parse_frontend_mutation_source(
2531 &new_source,
2532 "Error parsing KCL source after adding circle",
2533 "No AST produced after adding circle",
2534 )?;
2535
2536 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2537 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2538 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2539 )))
2540 })?;
2541
2542 self.program = new_program.clone();
2544
2545 let mut truncated_program = new_program;
2547 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2548 .map_err(KclErrorWithOutputs::no_outputs)?;
2549
2550 let outcome = ctx
2552 .run_mock(
2553 &truncated_program,
2554 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2555 )
2556 .await?;
2557
2558 let new_object_ids = {
2559 let make_err =
2560 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2561 let segment_id = outcome
2562 .source_range_to_object
2563 .get(&circle_node_ref.range)
2564 .copied()
2565 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2566 let segment_object = outcome
2567 .scene_objects
2568 .get(segment_id.0)
2569 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2570 let ObjectKind::Segment { segment } = &segment_object.kind else {
2571 return Err(make_err(format!(
2572 "Object is not a segment, it is {}",
2573 segment_object.kind.human_friendly_kind_with_article()
2574 )));
2575 };
2576 let Segment::Circle(circle) = segment else {
2577 return Err(make_err(format!(
2578 "Segment is not a circle, it is {}",
2579 segment.human_friendly_kind_with_article()
2580 )));
2581 };
2582 vec![circle.start, circle.center, segment_id]
2583 };
2584 let src_delta = SourceDelta { text: new_source };
2585 let outcome = self.update_state_after_exec(outcome, false);
2587 let scene_graph_delta = SceneGraphDelta {
2588 new_graph: self.scene_graph_for_ui(),
2589 invalidates_ids: false,
2590 new_objects: new_object_ids,
2591 exec_outcome: outcome,
2592 };
2593 Ok((src_delta, scene_graph_delta))
2594 }
2595
2596 async fn add_control_point_spline(
2597 &mut self,
2598 ctx: &ExecutorContext,
2599 sketch: ObjectId,
2600 ctor: ControlPointSplineCtor,
2601 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2602 let new_program = ensure_control_point_spline_experimental_features(&self.program)
2603 .map_err(KclErrorWithOutputs::no_outputs)?;
2604
2605 let points_ast = to_ast_point2d_array(&ctor.points)
2606 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2607 let mut arguments = vec![ast::LabeledArg {
2608 label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2609 arg: points_ast,
2610 }];
2611 if ctor.construction == Some(true) {
2612 arguments.push(ast::LabeledArg {
2613 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2614 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2615 value: ast::LiteralValue::Bool(true),
2616 raw: "true".to_string(),
2617 digest: None,
2618 }))),
2619 });
2620 }
2621 let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2622 callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2623 unlabeled: None,
2624 arguments,
2625 digest: None,
2626 non_code_meta: Default::default(),
2627 })));
2628
2629 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2630 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2631 })?;
2632 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2633 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2634 "Object is not a sketch, it is {}",
2635 sketch_object.kind.human_friendly_kind_with_article(),
2636 ))));
2637 };
2638
2639 let mut new_ast = new_program.ast.clone();
2640 let (sketch_block_ref, _) = self
2641 .mutate_ast(
2642 &mut new_ast,
2643 sketch,
2644 AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2645 )
2646 .map_err(KclErrorWithOutputs::no_outputs)?;
2647 let new_source = source_from_ast(&new_ast);
2648 let new_program = parse_frontend_mutation_source(
2649 &new_source,
2650 "Error parsing KCL source after adding controlPointSpline",
2651 "No AST produced after adding controlPointSpline",
2652 )?;
2653
2654 let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2655 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2656 "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2657 )))
2658 })?;
2659
2660 self.program = new_program.clone();
2661
2662 let mut truncated_program = new_program;
2663 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2664 .map_err(KclErrorWithOutputs::no_outputs)?;
2665
2666 let outcome = ctx
2667 .run_mock(
2668 &truncated_program,
2669 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2670 )
2671 .await?;
2672
2673 let new_object_ids = {
2674 let make_err =
2675 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2676 let segment_id = outcome
2677 .source_range_to_object
2678 .get(&spline_node_ref.range)
2679 .copied()
2680 .ok_or_else(|| {
2681 make_err(format!(
2682 "Source range of controlPointSpline not found: {spline_node_ref:?}"
2683 ))
2684 })?;
2685 let segment_object = outcome
2686 .scene_objects
2687 .get(segment_id.0)
2688 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2689 let ObjectKind::Segment { segment } = &segment_object.kind else {
2690 return Err(make_err(format!(
2691 "Object is not a segment, it is {}",
2692 segment_object.kind.human_friendly_kind_with_article()
2693 )));
2694 };
2695 let Segment::ControlPointSpline(spline) = segment else {
2696 return Err(make_err(format!(
2697 "Segment is not a control point spline, it is {}",
2698 segment.human_friendly_kind_with_article()
2699 )));
2700 };
2701
2702 let mut ids = outcome
2703 .scene_objects
2704 .iter()
2705 .filter_map(|obj| match &obj.kind {
2706 ObjectKind::Segment {
2707 segment: Segment::Line(line),
2708 } if line.owner == Some(segment_id) => Some(obj.id),
2709 _ => None,
2710 })
2711 .collect::<Vec<_>>();
2712 ids.extend(spline.controls.clone());
2713 ids.push(segment_id);
2714 ids
2715 };
2716 let src_delta = SourceDelta { text: new_source };
2717 let outcome = self.update_state_after_exec(outcome, false);
2718 let scene_graph_delta = SceneGraphDelta {
2719 new_graph: self.scene_graph_for_ui(),
2720 invalidates_ids: false,
2721 new_objects: new_object_ids,
2722 exec_outcome: outcome,
2723 };
2724 Ok((src_delta, scene_graph_delta))
2725 }
2726
2727 fn edit_point(
2728 &mut self,
2729 new_ast: &mut ast::Node<ast::Program>,
2730 sketch: ObjectId,
2731 point: ObjectId,
2732 ctor: PointCtor,
2733 ) -> Result<(), KclError> {
2734 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2736
2737 let sketch_id = sketch;
2739 let sketch_object = self
2740 .scene_graph
2741 .objects
2742 .get(sketch_id.0)
2743 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2744 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2745 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2746 };
2747 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2748 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2749 })?;
2750 let point_id = point;
2752 let point_object = self
2753 .scene_graph
2754 .objects
2755 .get(point_id.0)
2756 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2757 let ObjectKind::Segment {
2758 segment: Segment::Point(point),
2759 } = &point_object.kind
2760 else {
2761 return Err(KclError::refactor(format!(
2762 "Object is not a point segment: {point_object:?}"
2763 )));
2764 };
2765
2766 if let Some(owner_id) = point.owner {
2768 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2769 KclError::refactor(format!(
2770 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2771 ))
2772 })?;
2773 let ObjectKind::Segment { segment } = &owner_object.kind else {
2774 return Err(KclError::refactor(format!(
2775 "Internal: Owner of point is not a segment, but found {}",
2776 owner_object.kind.human_friendly_kind_with_article()
2777 )));
2778 };
2779
2780 if let Segment::Line(line) = segment {
2782 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2783 return Err(KclError::refactor(format!(
2784 "Internal: Owner of point does not have line ctor, but found {}",
2785 line.ctor.human_friendly_kind_with_article()
2786 )));
2787 };
2788 let mut line_ctor = line_ctor.clone();
2789 if line.start == point_id {
2791 line_ctor.start = ctor.position;
2792 } else if line.end == point_id {
2793 line_ctor.end = ctor.position;
2794 } else {
2795 return Err(KclError::refactor(format!(
2796 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2797 )));
2798 }
2799 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2800 }
2801
2802 if let Segment::Arc(arc) = segment {
2804 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2805 return Err(KclError::refactor(format!(
2806 "Internal: Owner of point does not have arc ctor, but found {}",
2807 arc.ctor.human_friendly_kind_with_article()
2808 )));
2809 };
2810 let mut arc_ctor = arc_ctor.clone();
2811 if arc.center == point_id {
2813 arc_ctor.center = ctor.position;
2814 } else if arc.start == point_id {
2815 arc_ctor.start = ctor.position;
2816 } else if arc.end == point_id {
2817 arc_ctor.end = ctor.position;
2818 } else {
2819 return Err(KclError::refactor(format!(
2820 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2821 )));
2822 }
2823 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2824 }
2825
2826 if let Segment::Circle(circle) = segment {
2828 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2829 return Err(KclError::refactor(format!(
2830 "Internal: Owner of point does not have circle ctor, but found {}",
2831 circle.ctor.human_friendly_kind_with_article()
2832 )));
2833 };
2834 let mut circle_ctor = circle_ctor.clone();
2835 if circle.center == point_id {
2836 circle_ctor.center = ctor.position;
2837 } else if circle.start == point_id {
2838 circle_ctor.start = ctor.position;
2839 } else {
2840 return Err(KclError::refactor(format!(
2841 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2842 )));
2843 }
2844 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2845 }
2846
2847 if let Segment::ControlPointSpline(spline) = segment {
2848 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2849 return Err(KclError::refactor(format!(
2850 "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2851 spline.ctor.human_friendly_kind_with_article()
2852 )));
2853 };
2854 let mut spline_ctor = spline_ctor.clone();
2855 let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2856 return Err(KclError::refactor(format!(
2857 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2858 )));
2859 };
2860 spline_ctor.points[control_index] = ctor.position;
2861 return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2862 }
2863
2864 }
2867
2868 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2870 Ok(())
2871 }
2872
2873 fn edit_line(
2874 &mut self,
2875 new_ast: &mut ast::Node<ast::Program>,
2876 sketch: ObjectId,
2877 line: ObjectId,
2878 ctor: LineCtor,
2879 ) -> Result<(), KclError> {
2880 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2882 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2883
2884 let sketch_id = sketch;
2886 let sketch_object = self
2887 .scene_graph
2888 .objects
2889 .get(sketch_id.0)
2890 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2891 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2892 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2893 };
2894 sketch
2895 .segments
2896 .iter()
2897 .find(|o| **o == line)
2898 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2899 let line_id = line;
2901 let line_object = self
2902 .scene_graph
2903 .objects
2904 .get(line_id.0)
2905 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2906 let ObjectKind::Segment { .. } = &line_object.kind else {
2907 let kind = line_object.kind.human_friendly_kind_with_article();
2908 return Err(KclError::refactor(format!(
2909 "This constraint only works on Segments, but you selected {kind}"
2910 )));
2911 };
2912
2913 self.mutate_ast(
2915 new_ast,
2916 line_id,
2917 AstMutateCommand::EditLine {
2918 start: new_start_ast,
2919 end: new_end_ast,
2920 construction: ctor.construction,
2921 },
2922 )?;
2923 Ok(())
2924 }
2925
2926 fn edit_arc(
2927 &mut self,
2928 new_ast: &mut ast::Node<ast::Program>,
2929 sketch: ObjectId,
2930 arc: ObjectId,
2931 ctor: ArcCtor,
2932 ) -> Result<(), KclError> {
2933 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2935 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2936 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2937
2938 let sketch_id = sketch;
2940 let sketch_object = self
2941 .scene_graph
2942 .objects
2943 .get(sketch_id.0)
2944 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2945 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2946 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2947 };
2948 sketch
2949 .segments
2950 .iter()
2951 .find(|o| **o == arc)
2952 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2953 let arc_id = arc;
2955 let arc_object = self
2956 .scene_graph
2957 .objects
2958 .get(arc_id.0)
2959 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2960 let ObjectKind::Segment { .. } = &arc_object.kind else {
2961 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2962 };
2963
2964 self.mutate_ast(
2966 new_ast,
2967 arc_id,
2968 AstMutateCommand::EditArc {
2969 start: new_start_ast,
2970 end: new_end_ast,
2971 center: new_center_ast,
2972 direction: ctor.direction,
2973 construction: ctor.construction,
2974 },
2975 )?;
2976 Ok(())
2977 }
2978
2979 fn edit_circle(
2980 &mut self,
2981 new_ast: &mut ast::Node<ast::Program>,
2982 sketch: ObjectId,
2983 circle: ObjectId,
2984 ctor: CircleCtor,
2985 ) -> Result<(), KclError> {
2986 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2988 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2989
2990 let sketch_id = sketch;
2992 let sketch_object = self
2993 .scene_graph
2994 .objects
2995 .get(sketch_id.0)
2996 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2997 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2998 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2999 };
3000 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3001 KclError::refactor(format!(
3002 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3003 ))
3004 })?;
3005 let circle_id = circle;
3007 let circle_object = self
3008 .scene_graph
3009 .objects
3010 .get(circle_id.0)
3011 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3012 let ObjectKind::Segment { .. } = &circle_object.kind else {
3013 return Err(KclError::refactor(format!(
3014 "Object is not a segment: {circle_object:?}"
3015 )));
3016 };
3017
3018 self.mutate_ast(
3020 new_ast,
3021 circle_id,
3022 AstMutateCommand::EditCircle {
3023 start: new_start_ast,
3024 center: new_center_ast,
3025 construction: ctor.construction,
3026 },
3027 )?;
3028 Ok(())
3029 }
3030
3031 fn edit_control_point_spline(
3032 &mut self,
3033 new_ast: &mut ast::Node<ast::Program>,
3034 sketch: ObjectId,
3035 spline: ObjectId,
3036 ctor: ControlPointSplineCtor,
3037 ) -> Result<(), KclError> {
3038 let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3039
3040 let sketch_object = self
3041 .scene_graph
3042 .objects
3043 .get(sketch.0)
3044 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3045 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3046 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3047 };
3048 sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3049 KclError::refactor(format!(
3050 "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3051 ))
3052 })?;
3053
3054 let spline_object =
3055 self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3056 KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3057 })?;
3058 let ObjectKind::Segment { .. } = &spline_object.kind else {
3059 return Err(KclError::refactor(format!(
3060 "Object is not a segment: {spline_object:?}"
3061 )));
3062 };
3063
3064 self.mutate_ast(
3065 new_ast,
3066 spline,
3067 AstMutateCommand::EditControlPointSpline {
3068 points: points_ast,
3069 construction: ctor.construction,
3070 },
3071 )?;
3072 Ok(())
3073 }
3074
3075 fn delete_segment(
3076 &mut self,
3077 new_ast: &mut ast::Node<ast::Program>,
3078 sketch: ObjectId,
3079 segment_id: ObjectId,
3080 ) -> Result<(), KclError> {
3081 let sketch_id = sketch;
3083 let sketch_object = self
3084 .scene_graph
3085 .objects
3086 .get(sketch_id.0)
3087 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3088 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3089 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3090 };
3091 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3092 KclError::refactor(format!(
3093 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3094 ))
3095 })?;
3096 let segment_object =
3098 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3099 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3100 })?;
3101 let ObjectKind::Segment { .. } = &segment_object.kind else {
3102 return Err(KclError::refactor(format!(
3103 "Object is not a segment, it is {}",
3104 segment_object.kind.human_friendly_kind_with_article()
3105 )));
3106 };
3107
3108 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3110 Ok(())
3111 }
3112
3113 fn delete_constraint(
3114 &mut self,
3115 new_ast: &mut ast::Node<ast::Program>,
3116 sketch: ObjectId,
3117 constraint_id: ObjectId,
3118 ) -> Result<(), KclError> {
3119 let sketch_id = sketch;
3121 let sketch_object = self
3122 .scene_graph
3123 .objects
3124 .get(sketch_id.0)
3125 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3126 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3127 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3128 };
3129 sketch
3130 .constraints
3131 .iter()
3132 .find(|o| **o == constraint_id)
3133 .ok_or_else(|| {
3134 KclError::refactor(format!(
3135 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3136 ))
3137 })?;
3138 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3140 KclError::refactor(format!(
3141 "Constraint not found in scene graph: constraint={constraint_id:?}"
3142 ))
3143 })?;
3144 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3145 return Err(KclError::refactor(format!(
3146 "Object is not a constraint, it is {}",
3147 constraint_object.kind.human_friendly_kind_with_article()
3148 )));
3149 };
3150
3151 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3153 Ok(())
3154 }
3155
3156 fn edit_coincident_constraint(
3157 &mut self,
3158 new_ast: &mut ast::Node<ast::Program>,
3159 constraint_id: ObjectId,
3160 segments: Vec<ConstraintSegment>,
3161 ) -> Result<(), KclError> {
3162 if segments.len() < 2 {
3163 return Err(KclError::refactor(format!(
3164 "Coincident constraint must have at least 2 inputs, got {}",
3165 segments.len()
3166 )));
3167 }
3168
3169 let segment_asts = segments
3170 .iter()
3171 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3172 .collect::<Result<Vec<_>, _>>()?;
3173
3174 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3175 elements: segment_asts,
3176 digest: None,
3177 non_code_meta: Default::default(),
3178 })));
3179
3180 self.mutate_ast(
3181 new_ast,
3182 constraint_id,
3183 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3184 )?;
3185 Ok(())
3186 }
3187
3188 fn edit_horizontal_points_constraint(
3189 &mut self,
3190 new_ast: &mut ast::Node<ast::Program>,
3191 constraint_id: ObjectId,
3192 points: Vec<ConstraintSegment>,
3193 ) -> Result<(), KclError> {
3194 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3195 }
3196
3197 fn edit_vertical_points_constraint(
3198 &mut self,
3199 new_ast: &mut ast::Node<ast::Program>,
3200 constraint_id: ObjectId,
3201 points: Vec<ConstraintSegment>,
3202 ) -> Result<(), KclError> {
3203 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3204 }
3205
3206 fn edit_axis_points_constraint(
3207 &mut self,
3208 new_ast: &mut ast::Node<ast::Program>,
3209 constraint_id: ObjectId,
3210 points: Vec<ConstraintSegment>,
3211 constraint_name: &str,
3212 ) -> Result<(), KclError> {
3213 if points.len() < 2 {
3214 return Err(KclError::refactor(format!(
3215 "{constraint_name} points constraint must have at least 2 points, got {}",
3216 points.len()
3217 )));
3218 }
3219
3220 let point_asts = points
3221 .iter()
3222 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3223 .collect::<Result<Vec<_>, _>>()?;
3224
3225 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3226 elements: point_asts,
3227 digest: None,
3228 non_code_meta: Default::default(),
3229 })));
3230
3231 self.mutate_ast(
3232 new_ast,
3233 constraint_id,
3234 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3235 )?;
3236 Ok(())
3237 }
3238
3239 fn edit_equal_length_constraint(
3241 &mut self,
3242 new_ast: &mut ast::Node<ast::Program>,
3243 constraint_id: ObjectId,
3244 lines: Vec<ObjectId>,
3245 ) -> Result<(), KclError> {
3246 if lines.len() < 2 {
3247 return Err(KclError::refactor(format!(
3248 "Lines equal length constraint must have at least 2 lines, got {}",
3249 lines.len()
3250 )));
3251 }
3252
3253 let line_asts = lines
3254 .iter()
3255 .map(|line_id| {
3256 let line_object = self
3257 .scene_graph
3258 .objects
3259 .get(line_id.0)
3260 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3261 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3262 let kind = line_object.kind.human_friendly_kind_with_article();
3263 return Err(KclError::refactor(format!(
3264 "This constraint only works on Segments, but you selected {kind}"
3265 )));
3266 };
3267 let Segment::Line(_) = line_segment else {
3268 let kind = line_segment.human_friendly_kind_with_article();
3269 return Err(KclError::refactor(format!(
3270 "Only lines can be made equal length, but you selected {kind}"
3271 )));
3272 };
3273
3274 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3275 })
3276 .collect::<Result<Vec<_>, _>>()?;
3277
3278 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3279 elements: line_asts,
3280 digest: None,
3281 non_code_meta: Default::default(),
3282 })));
3283
3284 self.mutate_ast(
3285 new_ast,
3286 constraint_id,
3287 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3288 )?;
3289 Ok(())
3290 }
3291
3292 fn edit_parallel_constraint(
3294 &mut self,
3295 new_ast: &mut ast::Node<ast::Program>,
3296 constraint_id: ObjectId,
3297 lines: Vec<ObjectId>,
3298 ) -> Result<(), KclError> {
3299 if lines.len() < 2 {
3300 return Err(KclError::refactor(format!(
3301 "Parallel constraint must have at least 2 lines, got {}",
3302 lines.len()
3303 )));
3304 }
3305
3306 let line_asts = lines
3307 .iter()
3308 .map(|line_id| {
3309 let line_object = self
3310 .scene_graph
3311 .objects
3312 .get(line_id.0)
3313 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3314 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3315 let kind = line_object.kind.human_friendly_kind_with_article();
3316 return Err(KclError::refactor(format!(
3317 "This constraint only works on Segments, but you selected {kind}"
3318 )));
3319 };
3320 let Segment::Line(_) = line_segment else {
3321 let kind = line_segment.human_friendly_kind_with_article();
3322 return Err(KclError::refactor(format!(
3323 "Only lines can be made parallel, but you selected {kind}"
3324 )));
3325 };
3326
3327 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3328 })
3329 .collect::<Result<Vec<_>, _>>()?;
3330
3331 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3332 elements: line_asts,
3333 digest: None,
3334 non_code_meta: Default::default(),
3335 })));
3336
3337 self.mutate_ast(
3338 new_ast,
3339 constraint_id,
3340 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3341 )?;
3342 Ok(())
3343 }
3344
3345 fn edit_equal_radius_constraint(
3347 &mut self,
3348 new_ast: &mut ast::Node<ast::Program>,
3349 constraint_id: ObjectId,
3350 input: Vec<ObjectId>,
3351 ) -> Result<(), KclError> {
3352 if input.len() < 2 {
3353 return Err(KclError::refactor(format!(
3354 "equalRadius constraint must have at least 2 segments, got {}",
3355 input.len()
3356 )));
3357 }
3358
3359 let input_asts = input
3360 .iter()
3361 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3362 .collect::<Result<Vec<_>, _>>()?;
3363
3364 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3365 elements: input_asts,
3366 digest: None,
3367 non_code_meta: Default::default(),
3368 })));
3369
3370 self.mutate_ast(
3371 new_ast,
3372 constraint_id,
3373 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3374 )?;
3375 Ok(())
3376 }
3377
3378 async fn execute_after_edit(
3379 &mut self,
3380 ctx: &ExecutorContext,
3381 sketch: ObjectId,
3382 sketch_block_ref: AstNodeRef,
3383 new_ast: &mut ast::Node<ast::Program>,
3384 options: ExecuteAfterEditOptions,
3385 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3386 let ExecuteAfterEditOptions {
3387 segment_ids_edited,
3388 edit_kind,
3389 commit_solved_initial_guesses,
3390 } = options;
3391
3392 let new_source = source_from_ast(new_ast);
3394 let new_program = parse_frontend_mutation_source(
3396 &new_source,
3397 "Error parsing KCL source after editing",
3398 "No AST produced after editing",
3399 )?;
3400
3401 let is_delete = edit_kind.is_delete();
3403 let truncated_program = {
3404 let mut truncated_program = new_program.clone();
3405 only_sketch_block(
3406 &mut truncated_program.ast,
3407 &sketch_block_ref,
3408 edit_kind.to_change_kind(),
3409 )
3410 .map_err(KclErrorWithOutputs::no_outputs)?;
3411 truncated_program
3412 };
3413
3414 let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3416 let mock_config = MockConfig {
3417 sketch_block_id: Some(sketch),
3418 freedom_analysis: is_delete,
3419 segment_ids_edited: segment_ids_edited.clone(),
3420 drag_anchors,
3421 ..Default::default()
3422 };
3423 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3424
3425 self.program = new_program;
3427
3428 let outcome = self.update_state_after_exec(outcome, is_delete);
3430
3431 let src_delta = if commit_solved_initial_guesses {
3432 self.commit_var_solutions_to_program(&outcome, "editing")?
3433 } else {
3434 SourceDelta { text: new_source }
3435 };
3436 let scene_graph_delta = SceneGraphDelta {
3437 new_graph: self.scene_graph_for_ui(),
3438 invalidates_ids: is_delete,
3439 new_objects: Vec::new(),
3440 exec_outcome: outcome,
3441 };
3442 Ok((src_delta, scene_graph_delta))
3443 }
3444
3445 async fn execute_after_delete_sketch(
3446 &mut self,
3447 ctx: &ExecutorContext,
3448 new_ast: &mut ast::Node<ast::Program>,
3449 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3450 let new_source = source_from_ast(new_ast);
3452 let new_program = parse_frontend_mutation_source(
3454 &new_source,
3455 "Error parsing KCL source after editing",
3456 "No AST produced after editing",
3457 )?;
3458
3459 self.program = new_program.clone();
3461
3462 let outcome = ctx.run_with_caching(new_program).await?;
3468 let freedom_analysis_ran = true;
3469
3470 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3471
3472 let src_delta = SourceDelta { text: new_source };
3473 let scene_graph_delta = SceneGraphDelta {
3474 new_graph: self.scene_graph_for_ui(),
3475 invalidates_ids: true,
3476 new_objects: Vec::new(),
3477 exec_outcome: outcome,
3478 };
3479 Ok((src_delta, scene_graph_delta))
3480 }
3481
3482 fn point_id_to_ast_reference(
3487 &self,
3488 point_id: ObjectId,
3489 new_ast: &mut ast::Node<ast::Program>,
3490 ) -> Result<ast::Expr, KclError> {
3491 let point_object = self
3492 .scene_graph
3493 .objects
3494 .get(point_id.0)
3495 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3496 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3497 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3498 };
3499 let Segment::Point(point) = point_segment else {
3500 return Err(KclError::refactor(format!(
3501 "Only points are currently supported: {point_object:?}"
3502 )));
3503 };
3504
3505 if let Some(owner_id) = point.owner {
3506 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3507 KclError::refactor(format!(
3508 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3509 ))
3510 })?;
3511 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3512 return Err(KclError::refactor(format!(
3513 "Owner of point is not a segment, but found {}",
3514 owner_object.kind.human_friendly_kind_with_article()
3515 )));
3516 };
3517
3518 match owner_segment {
3519 Segment::Line(line) => {
3520 let property = if line.start == point_id {
3521 LINE_PROPERTY_START
3522 } else if line.end == point_id {
3523 LINE_PROPERTY_END
3524 } else {
3525 return Err(KclError::refactor(format!(
3526 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3527 )));
3528 };
3529 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3530 }
3531 Segment::Arc(arc) => {
3532 let property = if arc.start == point_id {
3533 ARC_PROPERTY_START
3534 } else if arc.end == point_id {
3535 ARC_PROPERTY_END
3536 } else if arc.center == point_id {
3537 ARC_PROPERTY_CENTER
3538 } else {
3539 return Err(KclError::refactor(format!(
3540 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3541 )));
3542 };
3543 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3544 }
3545 Segment::Circle(circle) => {
3546 let property = if circle.start == point_id {
3547 CIRCLE_PROPERTY_START
3548 } else if circle.center == point_id {
3549 CIRCLE_PROPERTY_CENTER
3550 } else {
3551 return Err(KclError::refactor(format!(
3552 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3553 )));
3554 };
3555 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3556 }
3557 Segment::ControlPointSpline(spline) => {
3558 let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3559 return Err(KclError::refactor(format!(
3560 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3561 )));
3562 };
3563 let owner_expr =
3564 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3565 let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3566 Ok(create_index_expression(controls_expr, index))
3567 }
3568 _ => Err(KclError::refactor(format!(
3569 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3570 ))),
3571 }
3572 } else {
3573 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3575 }
3576 }
3577
3578 fn line_id_to_ast_reference(
3579 &self,
3580 line_id: ObjectId,
3581 new_ast: &mut ast::Node<ast::Program>,
3582 ) -> Result<ast::Expr, KclError> {
3583 let line_object = self
3584 .scene_graph
3585 .objects
3586 .get(line_id.0)
3587 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3588 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3589 return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3590 };
3591 let Segment::Line(line) = line_segment else {
3592 return Err(KclError::refactor(format!(
3593 "Only lines are currently supported: {line_object:?}"
3594 )));
3595 };
3596
3597 if let Some(owner_id) = line.owner {
3598 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3599 KclError::refactor(format!(
3600 "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3601 ))
3602 })?;
3603 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3604 return Err(KclError::refactor(format!(
3605 "Owner of line is not a segment, but found {}",
3606 owner_object.kind.human_friendly_kind_with_article()
3607 )));
3608 };
3609
3610 match owner_segment {
3611 Segment::ControlPointSpline(spline) => {
3612 let Some(index) = spline
3613 .controls
3614 .windows(2)
3615 .position(|window| window[0] == line.start && window[1] == line.end)
3616 else {
3617 return Err(KclError::refactor(format!(
3618 "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3619 )));
3620 };
3621 let owner_expr =
3622 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3623 let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3624 Ok(create_index_expression(edges_expr, index))
3625 }
3626 _ => Err(KclError::refactor(format!(
3627 "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3628 ))),
3629 }
3630 } else {
3631 get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3632 }
3633 }
3634
3635 fn coincident_segment_to_ast(
3636 &self,
3637 segment: &ConstraintSegment,
3638 new_ast: &mut ast::Node<ast::Program>,
3639 ) -> Result<ast::Expr, KclError> {
3640 match segment {
3641 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3642 ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3643 }
3644 }
3645
3646 fn segment_id_to_constraint_ast_reference(
3647 &self,
3648 segment_id: ObjectId,
3649 new_ast: &mut ast::Node<ast::Program>,
3650 ) -> Result<ast::Expr, KclError> {
3651 let segment_object = self
3652 .scene_graph
3653 .objects
3654 .get(segment_id.0)
3655 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3656 let ObjectKind::Segment { segment } = &segment_object.kind else {
3657 return Err(KclError::refactor(format!(
3658 "Object is not a segment, it is {}",
3659 segment_object.kind.human_friendly_kind_with_article()
3660 )));
3661 };
3662
3663 match segment {
3664 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3665 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3666 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3667 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3668 Segment::ControlPointSpline(_) => {
3669 get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3670 }
3671 }
3672 }
3673
3674 fn axis_constraint_segment_to_ast(
3675 &self,
3676 segment: &ConstraintSegment,
3677 new_ast: &mut ast::Node<ast::Program>,
3678 ) -> Result<ast::Expr, KclError> {
3679 match segment {
3680 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3681 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3682 }
3683 }
3684
3685 async fn add_coincident(
3686 &mut self,
3687 sketch: ObjectId,
3688 coincident: Coincident,
3689 new_ast: &mut ast::Node<ast::Program>,
3690 ) -> Result<AstNodeRef, KclError> {
3691 let sketch_id = sketch;
3692 for segment in &coincident.segments {
3693 let ConstraintSegment::Segment(segment_id) = segment else {
3694 continue;
3695 };
3696 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3697 continue;
3698 };
3699 if matches!(
3700 segment_object.kind,
3701 ObjectKind::Segment {
3702 segment: Segment::ControlPointSpline(_)
3703 }
3704 ) {
3705 return Err(KclError::refactor(
3706 "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3707 .to_owned(),
3708 ));
3709 }
3710 }
3711 let segment_asts = coincident
3712 .segments
3713 .iter()
3714 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3715 .collect::<Result<Vec<_>, _>>()?;
3716 if segment_asts.len() < 2 {
3717 return Err(KclError::refactor(format!(
3718 "Coincident constraint must have at least 2 inputs, got {}",
3719 segment_asts.len()
3720 )));
3721 }
3722
3723 let coincident_ast = create_coincident_ast(segment_asts);
3725
3726 let (sketch_block_ref, _) = self.mutate_ast(
3728 new_ast,
3729 sketch_id,
3730 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3731 )?;
3732 Ok(sketch_block_ref)
3733 }
3734
3735 async fn add_distance(
3736 &mut self,
3737 sketch: ObjectId,
3738 distance: Distance,
3739 new_ast: &mut ast::Node<ast::Program>,
3740 ) -> Result<AstNodeRef, KclError> {
3741 self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3742 }
3743
3744 fn distance_constraint_ast_parts(
3745 &self,
3746 function_name: &str,
3747 distance: &Distance,
3748 new_ast: &mut ast::Node<ast::Program>,
3749 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3750 let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3751 [pt0, pt1] => [
3752 self.coincident_segment_to_ast(pt0, new_ast)?,
3753 self.coincident_segment_to_ast(pt1, new_ast)?,
3754 ],
3755 _ => {
3756 return Err(KclError::refactor(format!(
3757 "Distance constraint must have exactly 2 segments, got {}",
3758 distance.segments.len()
3759 )));
3760 }
3761 };
3762
3763 let arguments = match &distance.label_position {
3764 Some(label_position) => vec![ast::LabeledArg {
3765 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3766 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3767 }],
3768 None => Default::default(),
3769 };
3770
3771 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3772 callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3773 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3774 ast::ArrayExpression {
3775 elements: vec![segment0_ast, segment1_ast],
3776 digest: None,
3777 non_code_meta: Default::default(),
3778 },
3779 )))),
3780 arguments,
3781 digest: None,
3782 non_code_meta: Default::default(),
3783 })));
3784 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3785 value: ast::LiteralValue::Number {
3786 value: distance.distance.value,
3787 suffix: distance.distance.units,
3788 },
3789 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3790 KclError::refactor(format!(
3791 "Could not format numeric suffix: {:?}",
3792 distance.distance.units
3793 ))
3794 })?,
3795 digest: None,
3796 })));
3797
3798 Ok((call, value))
3799 }
3800
3801 fn add_distance_constraint(
3802 &mut self,
3803 sketch: ObjectId,
3804 function_name: &str,
3805 distance: Distance,
3806 new_ast: &mut ast::Node<ast::Program>,
3807 ) -> Result<AstNodeRef, KclError> {
3808 let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3809 let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3810 left: call,
3811 operator: ast::BinaryOperator::Eq,
3812 right: value,
3813 digest: None,
3814 })));
3815
3816 let (sketch_block_ref, _) = self.mutate_ast(
3817 new_ast,
3818 sketch,
3819 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3820 )?;
3821 Ok(sketch_block_ref)
3822 }
3823
3824 async fn add_angle(
3825 &mut self,
3826 sketch: ObjectId,
3827 angle: Angle,
3828 new_ast: &mut ast::Node<ast::Program>,
3829 ) -> Result<AstNodeRef, KclError> {
3830 let sketch_id = sketch;
3831 let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3832 let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3833 left: angle_call_ast,
3834 operator: ast::BinaryOperator::Eq,
3835 right: angle_value_ast,
3836 digest: None,
3837 })));
3838
3839 let (sketch_block_ref, _) = self.mutate_ast(
3841 new_ast,
3842 sketch_id,
3843 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3844 )?;
3845 Ok(sketch_block_ref)
3846 }
3847
3848 fn angle_constraint_ast_parts(
3849 &self,
3850 angle: &Angle,
3851 new_ast: &mut ast::Node<ast::Program>,
3852 ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3853 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3854 return Err(KclError::refactor(format!(
3855 "Angle constraint must have exactly 2 lines, got {}",
3856 angle.lines.len()
3857 )));
3858 };
3859
3860 let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3861 let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3862 let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3863 elements: vec![l0_ast, l1_ast],
3864 digest: None,
3865 non_code_meta: Default::default(),
3866 })));
3867
3868 if angle.inverse == Some(true) && angle.sector.is_none() {
3869 return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3870 }
3871
3872 let uses_angle_dimension = angle.sector.is_some();
3873 let mut arguments = if uses_angle_dimension {
3874 vec![ast::LabeledArg {
3875 label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3876 arg: lines_ast.clone(),
3877 }]
3878 } else {
3879 Default::default()
3880 };
3881
3882 if let Some(sector) = angle.sector {
3883 arguments.push(ast::LabeledArg {
3884 label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3885 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3886 value: ast::LiteralValue::Number {
3887 value: f64::from(sector),
3888 suffix: NumericSuffix::None,
3889 },
3890 raw: sector.to_string(),
3891 digest: None,
3892 }))),
3893 });
3894 }
3895
3896 if angle.inverse == Some(true) {
3897 arguments.push(ast::LabeledArg {
3898 label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3899 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3900 value: ast::LiteralValue::Bool(true),
3901 raw: true.to_string(),
3902 digest: None,
3903 }))),
3904 });
3905 }
3906
3907 if let Some(label_position) = &angle.label_position {
3908 arguments.push(ast::LabeledArg {
3909 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3910 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3911 });
3912 }
3913
3914 let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3915 callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3916 ANGLE_DIMENSION_FN
3917 } else {
3918 ANGLE_FN
3919 })),
3920 unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3921 arguments,
3922 digest: None,
3923 non_code_meta: Default::default(),
3924 })));
3925 let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3926 value: ast::LiteralValue::Number {
3927 value: angle.angle.value,
3928 suffix: angle.angle.units,
3929 },
3930 raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3931 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3932 digest: None,
3933 })));
3934
3935 Ok((call, value))
3936 }
3937
3938 async fn add_tangent(
3939 &mut self,
3940 sketch: ObjectId,
3941 tangent: Tangent,
3942 new_ast: &mut ast::Node<ast::Program>,
3943 ) -> Result<AstNodeRef, KclError> {
3944 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3945 return Err(KclError::refactor(format!(
3946 "Tangent constraint must have exactly 2 segments, got {}",
3947 tangent.input.len()
3948 )));
3949 };
3950 let sketch_id = sketch;
3951
3952 let seg0_object = self
3953 .scene_graph
3954 .objects
3955 .get(seg0_id.0)
3956 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3957 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3958 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3959 };
3960 let seg0_ast = match seg0_segment {
3961 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3962 self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3963 }
3964 _ => {
3965 return Err(KclError::refactor(format!(
3966 "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3967 )));
3968 }
3969 };
3970
3971 let seg1_object = self
3972 .scene_graph
3973 .objects
3974 .get(seg1_id.0)
3975 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3976 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3977 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3978 };
3979 let seg1_ast = match seg1_segment {
3980 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3981 self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3982 }
3983 _ => {
3984 return Err(KclError::refactor(format!(
3985 "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3986 )));
3987 }
3988 };
3989
3990 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3991 let (sketch_block_ref, _) = self.mutate_ast(
3992 new_ast,
3993 sketch_id,
3994 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3995 )?;
3996 Ok(sketch_block_ref)
3997 }
3998
3999 async fn add_symmetric(
4000 &mut self,
4001 sketch: ObjectId,
4002 symmetric: Symmetric,
4003 new_ast: &mut ast::Node<ast::Program>,
4004 ) -> Result<AstNodeRef, KclError> {
4005 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4006 return Err(KclError::refactor(format!(
4007 "Symmetric constraint must have exactly 2 inputs, got {}",
4008 symmetric.input.len()
4009 )));
4010 };
4011 let sketch_id = sketch;
4012
4013 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4014 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4015 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4016
4017 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4018 let (sketch_block_ref, _) = self.mutate_ast(
4019 new_ast,
4020 sketch_id,
4021 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4022 )?;
4023 Ok(sketch_block_ref)
4024 }
4025
4026 async fn add_midpoint(
4027 &mut self,
4028 sketch: ObjectId,
4029 midpoint: Midpoint,
4030 new_ast: &mut ast::Node<ast::Program>,
4031 ) -> Result<AstNodeRef, KclError> {
4032 let sketch_id = sketch;
4033 let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4034
4035 let segment_object = self
4036 .scene_graph
4037 .objects
4038 .get(midpoint.segment.0)
4039 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4040 let ObjectKind::Segment {
4041 segment: midpoint_segment,
4042 } = &segment_object.kind
4043 else {
4044 return Err(KclError::refactor(format!(
4045 "Object must be a segment, but it was {}",
4046 segment_object.kind.human_friendly_kind_with_article()
4047 )));
4048 };
4049 let segment_ast = match midpoint_segment {
4050 Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4051 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4052 _ => {
4053 return Err(KclError::refactor(format!(
4054 "Midpoint target must be a line or arc segment but it was {}",
4055 midpoint_segment.human_friendly_kind_with_article()
4056 )));
4057 }
4058 };
4059
4060 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4061 let (sketch_block_ref, _) = self.mutate_ast(
4062 new_ast,
4063 sketch_id,
4064 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4065 )?;
4066 Ok(sketch_block_ref)
4067 }
4068
4069 async fn add_equal_radius(
4070 &mut self,
4071 sketch: ObjectId,
4072 equal_radius: EqualRadius,
4073 new_ast: &mut ast::Node<ast::Program>,
4074 ) -> Result<AstNodeRef, KclError> {
4075 if equal_radius.input.len() < 2 {
4076 return Err(KclError::refactor(format!(
4077 "equalRadius constraint must have at least 2 segments, got {}",
4078 equal_radius.input.len()
4079 )));
4080 }
4081
4082 let sketch_id = sketch;
4083 let input_asts = equal_radius
4084 .input
4085 .iter()
4086 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4087 .collect::<Result<Vec<_>, _>>()?;
4088
4089 let equal_radius_ast = create_equal_radius_ast(input_asts);
4090 let (sketch_block_ref, _) = self.mutate_ast(
4091 new_ast,
4092 sketch_id,
4093 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4094 )?;
4095 Ok(sketch_block_ref)
4096 }
4097
4098 async fn add_radius(
4099 &mut self,
4100 sketch: ObjectId,
4101 radius: Radius,
4102 new_ast: &mut ast::Node<ast::Program>,
4103 ) -> Result<AstNodeRef, KclError> {
4104 let params = ArcSizeConstraintParams {
4105 points: vec![radius.arc],
4106 function_name: RADIUS_FN,
4107 value: radius.radius.value,
4108 units: radius.radius.units,
4109 label_position: radius.label_position,
4110 constraint_type_name: "Radius",
4111 };
4112 self.add_arc_size_constraint(sketch, params, new_ast).await
4113 }
4114
4115 async fn add_diameter(
4116 &mut self,
4117 sketch: ObjectId,
4118 diameter: Diameter,
4119 new_ast: &mut ast::Node<ast::Program>,
4120 ) -> Result<AstNodeRef, KclError> {
4121 let params = ArcSizeConstraintParams {
4122 points: vec![diameter.arc],
4123 function_name: DIAMETER_FN,
4124 value: diameter.diameter.value,
4125 units: diameter.diameter.units,
4126 label_position: diameter.label_position,
4127 constraint_type_name: "Diameter",
4128 };
4129 self.add_arc_size_constraint(sketch, params, new_ast).await
4130 }
4131
4132 async fn add_fixed_constraints(
4133 &mut self,
4134 sketch: ObjectId,
4135 points: Vec<FixedPoint>,
4136 new_ast: &mut ast::Node<ast::Program>,
4137 ) -> Result<AstNodeRef, KclError> {
4138 let mut sketch_block_ref = None;
4139
4140 for fixed_point in points {
4141 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4142 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4143 .map_err(|err| KclError::refactor(err.to_string()))?;
4144
4145 let (sketch_ref, _) = self.mutate_ast(
4146 new_ast,
4147 sketch,
4148 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4149 )?;
4150 sketch_block_ref = Some(sketch_ref);
4151 }
4152
4153 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4154 }
4155
4156 async fn add_arc_size_constraint(
4157 &mut self,
4158 sketch: ObjectId,
4159 params: ArcSizeConstraintParams,
4160 new_ast: &mut ast::Node<ast::Program>,
4161 ) -> Result<AstNodeRef, KclError> {
4162 let sketch_id = sketch;
4163
4164 if params.points.len() != 1 {
4166 return Err(KclError::refactor(format!(
4167 "{} constraint must have exactly 1 argument (an arc segment), got {}",
4168 params.constraint_type_name,
4169 params.points.len()
4170 )));
4171 }
4172
4173 let arc_id = params.points[0];
4174 let arc_object = self
4175 .scene_graph
4176 .objects
4177 .get(arc_id.0)
4178 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4179 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4180 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4181 };
4182 let ref_type = match arc_segment {
4183 Segment::Arc(_) => ARC_VARIABLE,
4184 Segment::Circle(_) => CIRCLE_VARIABLE,
4185 _ => {
4186 return Err(KclError::refactor(format!(
4187 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4188 params.constraint_type_name
4189 )));
4190 }
4191 };
4192 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4194 let arguments = match ¶ms.label_position {
4195 Some(label_position) => vec![ast::LabeledArg {
4196 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4197 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4198 }],
4199 None => Default::default(),
4200 };
4201
4202 let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4204 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4205 unlabeled: Some(arc_ast),
4206 arguments,
4207 digest: None,
4208 non_code_meta: Default::default(),
4209 })));
4210 let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4211 left: call_ast,
4212 operator: ast::BinaryOperator::Eq,
4213 right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4214 value: ast::LiteralValue::Number {
4215 value: params.value,
4216 suffix: params.units,
4217 },
4218 raw: format_number_literal(params.value, params.units, None)
4219 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4220 digest: None,
4221 }))),
4222 digest: None,
4223 })));
4224
4225 let (sketch_block_ref, _) = self.mutate_ast(
4227 new_ast,
4228 sketch_id,
4229 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4230 )?;
4231 Ok(sketch_block_ref)
4232 }
4233
4234 async fn add_horizontal_distance(
4235 &mut self,
4236 sketch: ObjectId,
4237 distance: Distance,
4238 new_ast: &mut ast::Node<ast::Program>,
4239 ) -> Result<AstNodeRef, KclError> {
4240 self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4241 }
4242
4243 async fn add_vertical_distance(
4244 &mut self,
4245 sketch: ObjectId,
4246 distance: Distance,
4247 new_ast: &mut ast::Node<ast::Program>,
4248 ) -> Result<AstNodeRef, KclError> {
4249 self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4250 }
4251
4252 async fn add_horizontal(
4253 &mut self,
4254 sketch: ObjectId,
4255 horizontal: Horizontal,
4256 new_ast: &mut ast::Node<ast::Program>,
4257 ) -> Result<AstNodeRef, KclError> {
4258 let sketch_id = sketch;
4259
4260 let first_arg_ast = match horizontal {
4262 Horizontal::Line { line } => {
4263 let line_object = self
4264 .scene_graph
4265 .objects
4266 .get(line.0)
4267 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4268 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4269 let kind = line_object.kind.human_friendly_kind_with_article();
4270 return Err(KclError::refactor(format!(
4271 "This constraint only works on Segments, but you selected {kind}"
4272 )));
4273 };
4274 let Segment::Line(_) = line_segment else {
4275 return Err(KclError::refactor(format!(
4276 "Only lines can be made horizontal, but you selected {}",
4277 line_segment.human_friendly_kind_with_article(),
4278 )));
4279 };
4280 self.line_id_to_ast_reference(line, new_ast)?
4281 }
4282 Horizontal::Points { points } => {
4283 let point_asts = points
4284 .iter()
4285 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4286 .collect::<Result<Vec<_>, _>>()?;
4287 ast::ArrayExpression::new(point_asts).into()
4288 }
4289 };
4290 let horizontal_ast = create_horizontal_ast(first_arg_ast);
4292
4293 let (sketch_block_ref, _) = self.mutate_ast(
4295 new_ast,
4296 sketch_id,
4297 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4298 )?;
4299 Ok(sketch_block_ref)
4300 }
4301
4302 async fn add_lines_equal_length(
4303 &mut self,
4304 sketch: ObjectId,
4305 lines_equal_length: LinesEqualLength,
4306 new_ast: &mut ast::Node<ast::Program>,
4307 ) -> Result<AstNodeRef, KclError> {
4308 if lines_equal_length.lines.len() < 2 {
4309 return Err(KclError::refactor(format!(
4310 "Lines equal length constraint must have at least 2 lines, got {}",
4311 lines_equal_length.lines.len()
4312 )));
4313 };
4314
4315 let sketch_id = sketch;
4316
4317 let line_asts = lines_equal_length
4319 .lines
4320 .iter()
4321 .map(|line_id| {
4322 let line_object = self
4323 .scene_graph
4324 .objects
4325 .get(line_id.0)
4326 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4327 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4328 let kind = line_object.kind.human_friendly_kind_with_article();
4329 return Err(KclError::refactor(format!(
4330 "This constraint only works on Segments, but you selected {kind}"
4331 )));
4332 };
4333 let Segment::Line(_) = line_segment else {
4334 let kind = line_segment.human_friendly_kind_with_article();
4335 return Err(KclError::refactor(format!(
4336 "Only lines can be made equal length, but you selected {kind}"
4337 )));
4338 };
4339
4340 self.line_id_to_ast_reference(*line_id, new_ast)
4341 })
4342 .collect::<Result<Vec<_>, _>>()?;
4343
4344 let equal_length_ast = create_equal_length_ast(line_asts);
4346
4347 let (sketch_block_ref, _) = self.mutate_ast(
4349 new_ast,
4350 sketch_id,
4351 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4352 )?;
4353 Ok(sketch_block_ref)
4354 }
4355
4356 fn equal_radius_segment_id_to_ast_reference(
4357 &mut self,
4358 segment_id: ObjectId,
4359 new_ast: &mut ast::Node<ast::Program>,
4360 ) -> Result<ast::Expr, KclError> {
4361 let segment_object = self
4362 .scene_graph
4363 .objects
4364 .get(segment_id.0)
4365 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4366 let ObjectKind::Segment { segment } = &segment_object.kind else {
4367 return Err(KclError::refactor(format!(
4368 "Object is not a segment, it was {}",
4369 segment_object.kind.human_friendly_kind_with_article()
4370 )));
4371 };
4372
4373 let ref_type = match segment {
4374 Segment::Arc(_) => ARC_VARIABLE,
4375 Segment::Circle(_) => CIRCLE_VARIABLE,
4376 _ => {
4377 return Err(KclError::refactor(format!(
4378 "equalRadius supports only arc/circle segments, got {}",
4379 segment.human_friendly_kind_with_article()
4380 )));
4381 }
4382 };
4383
4384 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4385 }
4386
4387 fn symmetric_input_id_to_ast_reference(
4388 &mut self,
4389 segment_id: ObjectId,
4390 new_ast: &mut ast::Node<ast::Program>,
4391 ) -> Result<ast::Expr, KclError> {
4392 let segment_object = self
4393 .scene_graph
4394 .objects
4395 .get(segment_id.0)
4396 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4397 let ObjectKind::Segment { segment } = &segment_object.kind else {
4398 return Err(KclError::refactor(format!(
4399 "Object is not a segment, it was {}",
4400 segment_object.kind.human_friendly_kind_with_article()
4401 )));
4402 };
4403
4404 match segment {
4405 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4406 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4407 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4408 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4409 Segment::ControlPointSpline(_) => Err(KclError::refactor(
4410 "Symmetric does not yet support control point splines".to_owned(),
4411 )),
4412 }
4413 }
4414
4415 fn symmetric_axis_id_to_ast_reference(
4416 &mut self,
4417 segment_id: ObjectId,
4418 new_ast: &mut ast::Node<ast::Program>,
4419 ) -> Result<ast::Expr, KclError> {
4420 let segment_object = self
4421 .scene_graph
4422 .objects
4423 .get(segment_id.0)
4424 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4425 let ObjectKind::Segment { segment } = &segment_object.kind else {
4426 return Err(KclError::refactor(format!(
4427 "Object is not a segment, it was {}",
4428 segment_object.kind.human_friendly_kind_with_article()
4429 )));
4430 };
4431 match segment {
4432 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4433 _ => Err(KclError::refactor(format!(
4434 "Symmetric axis must be a line, got {}",
4435 segment.human_friendly_kind_with_article()
4436 ))),
4437 }
4438 }
4439
4440 async fn add_parallel(
4441 &mut self,
4442 sketch: ObjectId,
4443 parallel: Parallel,
4444 new_ast: &mut ast::Node<ast::Program>,
4445 ) -> Result<AstNodeRef, KclError> {
4446 if parallel.lines.len() < 2 {
4447 return Err(KclError::refactor(format!(
4448 "Parallel constraint must have at least 2 lines, got {}",
4449 parallel.lines.len()
4450 )));
4451 };
4452
4453 let sketch_id = sketch;
4454
4455 let line_asts = parallel
4456 .lines
4457 .iter()
4458 .map(|line_id| {
4459 let line_object = self
4460 .scene_graph
4461 .objects
4462 .get(line_id.0)
4463 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4464 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4465 let kind = line_object.kind.human_friendly_kind_with_article();
4466 return Err(KclError::refactor(format!(
4467 "This constraint only works on Segments, but you selected {kind}"
4468 )));
4469 };
4470 let Segment::Line(_) = line_segment else {
4471 let kind = line_segment.human_friendly_kind_with_article();
4472 return Err(KclError::refactor(format!(
4473 "Only lines can be made parallel, but you selected {kind}"
4474 )));
4475 };
4476
4477 self.line_id_to_ast_reference(*line_id, new_ast)
4478 })
4479 .collect::<Result<Vec<_>, _>>()?;
4480
4481 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4482 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4483 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4484 ast::ArrayExpression {
4485 elements: line_asts,
4486 digest: None,
4487 non_code_meta: Default::default(),
4488 },
4489 )))),
4490 arguments: Default::default(),
4491 digest: None,
4492 non_code_meta: Default::default(),
4493 })));
4494
4495 let (sketch_block_ref, _) = self.mutate_ast(
4496 new_ast,
4497 sketch_id,
4498 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4499 )?;
4500 Ok(sketch_block_ref)
4501 }
4502
4503 async fn add_perpendicular(
4504 &mut self,
4505 sketch: ObjectId,
4506 perpendicular: Perpendicular,
4507 new_ast: &mut ast::Node<ast::Program>,
4508 ) -> Result<AstNodeRef, KclError> {
4509 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4510 .await
4511 }
4512
4513 async fn add_lines_at_angle_constraint(
4514 &mut self,
4515 sketch: ObjectId,
4516 angle_kind: LinesAtAngleKind,
4517 lines: Vec<ObjectId>,
4518 new_ast: &mut ast::Node<ast::Program>,
4519 ) -> Result<AstNodeRef, KclError> {
4520 let &[line0_id, line1_id] = lines.as_slice() else {
4521 return Err(KclError::refactor(format!(
4522 "{} constraint must have exactly 2 lines, got {}",
4523 angle_kind.to_function_name(),
4524 lines.len()
4525 )));
4526 };
4527
4528 let sketch_id = sketch;
4529
4530 let line0_object = self
4532 .scene_graph
4533 .objects
4534 .get(line0_id.0)
4535 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4536 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4537 let kind = line0_object.kind.human_friendly_kind_with_article();
4538 return Err(KclError::refactor(format!(
4539 "This constraint only works on Segments, but you selected {kind}"
4540 )));
4541 };
4542 let Segment::Line(_) = line0_segment else {
4543 return Err(KclError::refactor(format!(
4544 "Only lines can be made {}, but you selected {}",
4545 angle_kind.to_function_name(),
4546 line0_segment.human_friendly_kind_with_article(),
4547 )));
4548 };
4549 let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4550
4551 let line1_object = self
4552 .scene_graph
4553 .objects
4554 .get(line1_id.0)
4555 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4556 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4557 let kind = line1_object.kind.human_friendly_kind_with_article();
4558 return Err(KclError::refactor(format!(
4559 "This constraint only works on Segments, but you selected {kind}"
4560 )));
4561 };
4562 let Segment::Line(_) = line1_segment else {
4563 return Err(KclError::refactor(format!(
4564 "Only lines can be made {}, but you selected {}",
4565 angle_kind.to_function_name(),
4566 line1_segment.human_friendly_kind_with_article(),
4567 )));
4568 };
4569 let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4570
4571 let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4573 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4574 unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4575 ast::ArrayExpression {
4576 elements: vec![line0_ast, line1_ast],
4577 digest: None,
4578 non_code_meta: Default::default(),
4579 },
4580 )))),
4581 arguments: Default::default(),
4582 digest: None,
4583 non_code_meta: Default::default(),
4584 })));
4585
4586 let (sketch_block_ref, _) = self.mutate_ast(
4588 new_ast,
4589 sketch_id,
4590 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4591 )?;
4592 Ok(sketch_block_ref)
4593 }
4594
4595 async fn add_vertical(
4596 &mut self,
4597 sketch: ObjectId,
4598 vertical: Vertical,
4599 new_ast: &mut ast::Node<ast::Program>,
4600 ) -> Result<AstNodeRef, KclError> {
4601 let sketch_id = sketch;
4602
4603 let first_arg_ast = match vertical {
4604 Vertical::Line { line } => {
4605 let line_object = self
4607 .scene_graph
4608 .objects
4609 .get(line.0)
4610 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4611 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4612 let kind = line_object.kind.human_friendly_kind_with_article();
4613 return Err(KclError::refactor(format!(
4614 "This constraint only works on Segments, but you selected {kind}"
4615 )));
4616 };
4617 let Segment::Line(_) = line_segment else {
4618 return Err(KclError::refactor(format!(
4619 "Only lines can be made vertical, but you selected {}",
4620 line_segment.human_friendly_kind_with_article()
4621 )));
4622 };
4623 self.line_id_to_ast_reference(line, new_ast)?
4624 }
4625 Vertical::Points { points } => {
4626 let point_asts = points
4627 .iter()
4628 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4629 .collect::<Result<Vec<_>, _>>()?;
4630 ast::ArrayExpression::new(point_asts).into()
4631 }
4632 };
4633 let vertical_ast = create_vertical_ast(first_arg_ast);
4635
4636 let (sketch_block_ref, _) = self.mutate_ast(
4638 new_ast,
4639 sketch_id,
4640 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4641 )?;
4642 Ok(sketch_block_ref)
4643 }
4644
4645 async fn execute_after_add_constraint(
4646 &mut self,
4647 ctx: &ExecutorContext,
4648 sketch_id: ObjectId,
4649 sketch_block_ref: AstNodeRef,
4650 new_ast: &mut ast::Node<ast::Program>,
4651 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4652 let new_source = source_from_ast(new_ast);
4654 let new_program = parse_frontend_mutation_source(
4656 &new_source,
4657 "Error parsing KCL source after adding constraint",
4658 "No AST produced after adding constraint",
4659 )?;
4660 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4661 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4662 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4663 )))
4664 })?;
4665
4666 let mut truncated_program = new_program.clone();
4669 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4670 .map_err(KclErrorWithOutputs::no_outputs)?;
4671
4672 let outcome = ctx
4674 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4675 .await?;
4676
4677 let new_object_ids = {
4678 let constraint_id = outcome
4680 .source_range_to_object
4681 .get(&constraint_node_ref.range)
4682 .copied()
4683 .ok_or_else(|| {
4684 KclErrorWithOutputs::from_error_outcome(
4685 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4686 outcome.clone(),
4687 )
4688 })?;
4689 vec![constraint_id]
4690 };
4691
4692 self.program = new_program;
4695
4696 let outcome = self.update_state_after_exec(outcome, true);
4698
4699 let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4700 let scene_graph_delta = SceneGraphDelta {
4701 new_graph: self.scene_graph_for_ui(),
4702 invalidates_ids: false,
4703 new_objects: new_object_ids,
4704 exec_outcome: outcome,
4705 };
4706 Ok((src_delta, scene_graph_delta))
4707 }
4708
4709 fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4710 let commit_failure = || {
4711 KclErrorWithOutputs::from_error_outcome(
4712 KclError::refactor(format!("Could not update KCL after {operation}.")),
4713 outcome.clone(),
4714 )
4715 };
4716
4717 let default_length_unit = self.default_length_unit();
4718 let mut settled_ast = self.program.ast.clone();
4719 let mut committed_solver_value = false;
4720 for (var_range, node_path, value) in &outcome.var_solutions {
4721 let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4722 return Err(commit_failure());
4723 };
4724 let new_value = match &lookup {
4725 Some(current_literal) => {
4726 if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4727 continue;
4728 }
4729 preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4730 }
4731 None => {
4732 Number {
4736 value: number_value_in_default_length_units(*value, default_length_unit),
4737 units: default_length_unit.into(),
4738 }
4739 }
4740 };
4741 committed_solver_value = true;
4742 let source_ref = SourceRef::Simple {
4743 range: *var_range,
4744 node_path: node_path.clone(),
4745 };
4746 mutate_ast_node_by_source_ref(
4747 &mut settled_ast,
4748 &source_ref,
4749 AstMutateCommand::EditVarInitialValue { value: new_value },
4750 )
4751 .map_err(|_| commit_failure())?;
4752 }
4753
4754 if !committed_solver_value {
4755 return Ok(SourceDelta {
4756 text: self.program.original_file_contents.clone(),
4757 });
4758 }
4759
4760 let settled_source = source_from_ast(&settled_ast);
4761 let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4762 if !errors.is_empty() {
4763 return Err(commit_failure());
4764 }
4765 let Some(settled_program) = settled_program else {
4766 return Err(commit_failure());
4767 };
4768
4769 self.program = settled_program;
4770
4771 Ok(SourceDelta { text: settled_source })
4772 }
4773
4774 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4776 if segment_ids_set.contains(&segment_id) {
4777 return true;
4778 }
4779
4780 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4781 return false;
4782 };
4783 let ObjectKind::Segment { segment } = &segment_object.kind else {
4784 return false;
4785 };
4786 let Segment::Point(point) = segment else {
4787 return false;
4788 };
4789
4790 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4791 }
4792
4793 fn remaining_constraint_segments(
4794 &self,
4795 segments: &[ConstraintSegment],
4796 segment_ids_set: &AhashIndexSet<ObjectId>,
4797 ) -> Vec<ConstraintSegment> {
4798 segments
4799 .iter()
4800 .copied()
4801 .filter(|segment| match segment {
4802 ConstraintSegment::Origin(_) => true,
4803 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4804 })
4805 .collect()
4806 }
4807
4808 fn find_referenced_constraints(
4809 &self,
4810 sketch_id: ObjectId,
4811 segment_ids_set: &AhashIndexSet<ObjectId>,
4812 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4813 let sketch_object = self
4815 .scene_graph
4816 .objects
4817 .get(sketch_id.0)
4818 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4819 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4820 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4821 };
4822 let segment_or_owner_matches = |segment_id: ObjectId| {
4823 if segment_ids_set.contains(&segment_id) {
4824 return true;
4825 }
4826 let segment_object = self.scene_graph.objects.get(segment_id.0);
4827 if let Some(obj) = segment_object
4828 && let ObjectKind::Segment { segment } = &obj.kind
4829 {
4830 match segment {
4831 Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4832 Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4833 _ => false,
4834 }
4835 } else {
4836 false
4837 }
4838 };
4839 let mut constraint_ids_set = AhashIndexSet::default();
4840 for constraint_id in &sketch.constraints {
4841 let constraint_object = self
4842 .scene_graph
4843 .objects
4844 .get(constraint_id.0)
4845 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4846 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4847 return Err(KclError::refactor(format!(
4848 "Object is not a constraint, it is {}",
4849 constraint_object.kind.human_friendly_kind_with_article()
4850 )));
4851 };
4852 let depends_on_segment = match constraint {
4853 Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4854 Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4855 Constraint::Fixed(fixed) => fixed
4856 .points
4857 .iter()
4858 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4859 Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4860 Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4861 Constraint::EqualRadius(equal_radius) => {
4862 equal_radius.input.iter().copied().any(segment_or_owner_matches)
4863 }
4864 Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4865 Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4866 Constraint::Horizontal(h) => match h {
4867 Horizontal::Line { line } => segment_or_owner_matches(*line),
4868 Horizontal::Points { points } => points.iter().any(|point| match point {
4869 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4870 ConstraintSegment::Origin(_) => false,
4871 }),
4872 },
4873 Constraint::Vertical(v) => match v {
4874 Vertical::Line { line } => segment_or_owner_matches(*line),
4875 Vertical::Points { points } => points.iter().any(|point| match point {
4876 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4877 ConstraintSegment::Origin(_) => false,
4878 }),
4879 },
4880 Constraint::LinesEqualLength(lines_equal_length) => {
4881 lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4882 }
4883 Constraint::Midpoint(midpoint) => {
4884 segment_or_owner_matches(midpoint.segment)
4885 || matches!(
4886 midpoint.point,
4887 ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4888 )
4889 }
4890 Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4891 Constraint::Perpendicular(perpendicular) => {
4892 perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4893 }
4894 Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4895 Constraint::Symmetric(symmetric) => {
4896 segment_or_owner_matches(symmetric.axis)
4897 || symmetric.input.iter().copied().any(segment_or_owner_matches)
4898 }
4899 Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4900 };
4901 if depends_on_segment {
4902 constraint_ids_set.insert(*constraint_id);
4903 }
4904 }
4905 Ok(constraint_ids_set)
4906 }
4907
4908 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4909 let mut outcome = outcome;
4910 self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4911 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4912
4913 if freedom_analysis_ran {
4914 self.point_freedom_cache.clear();
4917 for new_obj in &new_objects {
4918 if let ObjectKind::Segment {
4919 segment: crate::front::Segment::Point(point),
4920 } = &new_obj.kind
4921 {
4922 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4923 }
4924 }
4925 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4926 self.scene_graph.objects = new_objects;
4928 } else {
4929 for old_obj in &self.scene_graph.objects {
4932 if let ObjectKind::Segment {
4933 segment: crate::front::Segment::Point(point),
4934 } = &old_obj.kind
4935 {
4936 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4937 }
4938 }
4939
4940 let mut updated_objects = Vec::with_capacity(new_objects.len());
4942 for new_obj in new_objects {
4943 let mut obj = new_obj;
4944 if let ObjectKind::Segment {
4945 segment: crate::front::Segment::Point(point),
4946 } = &mut obj.kind
4947 {
4948 let new_freedom = point.freedom;
4949 match new_freedom {
4955 Freedom::Free => {
4956 match self.point_freedom_cache.get(&obj.id).copied() {
4957 Some(Freedom::Conflict) => {
4958 }
4961 Some(Freedom::Fixed) => {
4962 point.freedom = Freedom::Fixed;
4964 }
4965 Some(Freedom::Free) => {
4966 }
4968 None => {
4969 }
4971 }
4972 }
4973 Freedom::Fixed => {
4974 }
4976 Freedom::Conflict => {
4977 }
4979 }
4980 self.point_freedom_cache.insert(obj.id, point.freedom);
4982 }
4983 updated_objects.push(obj);
4984 }
4985
4986 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4987 self.scene_graph.objects = updated_objects;
4988 }
4989 outcome
4990 }
4991
4992 fn mutate_ast(
4993 &mut self,
4994 ast: &mut ast::Node<ast::Program>,
4995 object_id: ObjectId,
4996 command: AstMutateCommand,
4997 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4998 let sketch_object = self
4999 .scene_graph
5000 .objects
5001 .get(object_id.0)
5002 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5003 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5004 }
5005
5006 fn mutate_constraint_label_position(
5007 &mut self,
5008 ast: &mut ast::Node<ast::Program>,
5009 constraint_id: ObjectId,
5010 label_position: Point2d<Number>,
5011 ) -> Result<(), KclError> {
5012 let object = self
5013 .scene_graph
5014 .objects
5015 .get(constraint_id.0)
5016 .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5017 if !matches!(
5018 &object.kind,
5019 ObjectKind::Constraint {
5020 constraint: Constraint::Distance(_)
5021 | Constraint::HorizontalDistance(_)
5022 | Constraint::VerticalDistance(_)
5023 | Constraint::Radius(_)
5024 | Constraint::Diameter(_)
5025 | Constraint::Angle(_),
5026 }
5027 ) {
5028 return Err(KclError::refactor(format!(
5029 "Object does not support labelPosition: {constraint_id:?}"
5030 )));
5031 }
5032
5033 let label_position = to_ast_point2d_number(&label_position)
5034 .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5035 self.mutate_ast(
5036 ast,
5037 constraint_id,
5038 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5039 )?;
5040 Ok(())
5041 }
5042}
5043
5044fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5045 let sketch_object = scene_graph
5047 .objects
5048 .get(sketch_id.0)
5049 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5050 let ObjectKind::Sketch(_) = &sketch_object.kind else {
5051 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5052 };
5053 expect_single_node_ref(sketch_object)
5054}
5055
5056fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5057 match &object.source {
5058 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5059 range: *range,
5060 node_path: node_path.clone(),
5061 }),
5062 SourceRef::BackTrace { ranges } => {
5063 let [range] = ranges.as_slice() else {
5064 return Err(KclError::refactor(format!(
5065 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5066 ranges.len()
5067 )));
5068 };
5069 Ok(AstNodeRef {
5070 range: range.0,
5071 node_path: range.1.clone(),
5072 })
5073 }
5074 }
5075}
5076
5077fn only_sketch_block_from_range(
5080 ast: &mut ast::Node<ast::Program>,
5081 sketch_block_range: SourceRange,
5082 edit_kind: ChangeKind,
5083) -> Result<(), KclError> {
5084 let r1 = sketch_block_range;
5085 let matches_range = |r2: SourceRange| -> bool {
5086 match edit_kind {
5089 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5090 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5092 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5093 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5095 }
5096 };
5097 let mut found = false;
5098 for item in ast.body.iter_mut() {
5099 match item {
5100 ast::BodyItem::ImportStatement(_) => {}
5101 ast::BodyItem::ExpressionStatement(node) => {
5102 if matches_range(SourceRange::from(&*node))
5103 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5104 {
5105 sketch_block.is_being_edited = true;
5106 found = true;
5107 break;
5108 }
5109 }
5110 ast::BodyItem::VariableDeclaration(node) => {
5111 if matches_range(SourceRange::from(&node.declaration.init))
5112 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5113 {
5114 sketch_block.is_being_edited = true;
5115 found = true;
5116 break;
5117 }
5118 }
5119 ast::BodyItem::TypeDeclaration(_) => {}
5120 ast::BodyItem::ReturnStatement(node) => {
5121 if matches_range(SourceRange::from(&node.argument))
5122 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5123 {
5124 sketch_block.is_being_edited = true;
5125 found = true;
5126 break;
5127 }
5128 }
5129 }
5130 }
5131 if !found {
5132 return Err(KclError::refactor(format!(
5133 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5134 )));
5135 }
5136
5137 Ok(())
5138}
5139
5140fn only_sketch_block(
5141 ast: &mut ast::Node<ast::Program>,
5142 sketch_block_ref: &AstNodeRef,
5143 edit_kind: ChangeKind,
5144) -> Result<(), KclError> {
5145 let Some(target_node_path) = &sketch_block_ref.node_path else {
5146 #[cfg(target_arch = "wasm32")]
5147 web_sys::console::warn_1(
5148 &format!(
5149 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5150 sketch_block_ref
5151 )
5152 .into(),
5153 );
5154 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5155 };
5156 struct MarkSketchBlockBeingEdited<'a> {
5157 target_node_path: &'a ast::NodePath,
5158 }
5159
5160 impl Visitor for MarkSketchBlockBeingEdited<'_> {
5161 type Break = ();
5162 type Continue = ();
5163
5164 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5165 if let NodeMut::SketchBlock(sketch_block) = node
5166 && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5167 {
5168 sketch_block.is_being_edited = true;
5169 return TraversalReturn::new_break(());
5170 }
5171 TraversalReturn::new_continue(())
5172 }
5173
5174 fn finish(&mut self, _node: NodeMut<'_>) {}
5175 }
5176
5177 let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5178 let found = dfs_mut(ast, &mut marker).is_break();
5179 if !found {
5180 return Err(KclError::refactor(format!(
5181 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5182 )));
5183 }
5184
5185 Ok(())
5186}
5187
5188fn sketch_on_ast_expr(
5189 ast: &mut ast::Node<ast::Program>,
5190 scene_graph: &SceneGraph,
5191 solid_references: &HashMap<Uuid, SolidAstReference>,
5192 on: &Plane,
5193) -> Result<ast::Expr, KclError> {
5194 match on {
5195 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5196 Plane::Object(object_id) => {
5197 let on_object = scene_graph
5198 .objects
5199 .get(object_id.0)
5200 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5201 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5202 return Ok(face_expr);
5203 }
5204 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5205 }
5206 Plane::PrimitiveFace(face) => {
5207 let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5208 KclError::refactor(format!(
5209 "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5210 face.solid_id
5211 ))
5212 })?;
5213 let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5214 Ok(create_face_of_ast(solid_expr, face_id_expr))
5215 }
5216 }
5217}
5218
5219fn solid_references_from_variables(
5220 ast: &ast::Node<ast::Program>,
5221 variables: &IndexMap<String, KclValueView>,
5222) -> HashMap<Uuid, SolidAstReference> {
5223 let mut references = HashMap::new();
5224
5225 for item in &ast.body {
5228 let ast::BodyItem::VariableDeclaration(declaration) = item else {
5229 continue;
5230 };
5231 let name = &declaration.declaration.id.name;
5232 let Some(value) = variables.get(name) else {
5233 continue;
5234 };
5235
5236 match value {
5237 KclValueView::Solid { value } => {
5238 references.insert(
5239 value.id,
5240 SolidAstReference {
5241 variable_name: name.clone(),
5242 output_index: None,
5243 },
5244 );
5245 }
5246 KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5247 for (output_index, entry) in value.iter().enumerate() {
5248 if let KclValueView::Solid { value } = entry {
5249 references.insert(
5250 value.id,
5251 SolidAstReference {
5252 variable_name: name.clone(),
5253 output_index: Some(output_index),
5254 },
5255 );
5256 }
5257 }
5258 }
5259 _ => {}
5260 }
5261 }
5262
5263 references
5264}
5265
5266fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5267 let reference = solid_references.get(&solid_id)?;
5268 let solid_expr = ast_name_expr(reference.variable_name.clone());
5269 Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5270}
5271
5272fn sketch_face_of_scene_object_ast_expr(
5273 ast: &mut ast::Node<ast::Program>,
5274 on_object: &crate::front::Object,
5275) -> Result<Option<ast::Expr>, KclError> {
5276 match &on_object.kind {
5277 ObjectKind::Wall(wall) => {
5278 let solid_ref = get_or_insert_ast_reference(
5279 ast,
5280 &source_ref_from_source_ref_range(&wall.source.solid),
5281 "solid",
5282 None,
5283 )?;
5284 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5285 return Err(KclError::refactor(format!(
5286 "Could not resolve solid reference for selected wall: artifact_id={:?}",
5287 on_object.artifact_id
5288 )));
5289 };
5290 let solid_expr = indexed_solid_expr_for_sweep_output(
5291 ast_name_expr(solid_name_expr.name.name.clone()),
5292 wall.solid_output_index,
5293 );
5294 let sweep_ref = get_or_insert_ast_reference(
5295 ast,
5296 &source_ref_from_source_ref_range(&wall.source.sweep),
5297 "solid",
5298 None,
5299 )?;
5300 let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5301 return Err(KclError::refactor(format!(
5302 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5303 on_object.artifact_id
5304 )));
5305 };
5306 let sweep_name = sweep_name_expr.name.name.clone();
5307 let segment_ref = get_or_insert_ast_reference(
5308 ast,
5309 &source_ref_from_source_ref_range(&wall.source.segment),
5310 LINE_VARIABLE,
5311 None,
5312 )?;
5313
5314 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5315 wall.source
5316 .path
5317 .as_ref()
5318 .and_then(|path_source| region_name_from_path_source(ast, path_source))
5319 }) {
5320 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5321 return Err(KclError::refactor(format!(
5322 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5323 on_object.artifact_id
5324 )));
5325 };
5326 create_member_expression(
5327 create_member_expression(ast_name_expr(region_name), "tags"),
5328 &segment_name_expr.name.name,
5329 )
5330 } else {
5331 segment_ref
5332 };
5333
5334 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5335 }
5336 ObjectKind::Cap(cap) => {
5337 let solid_ref =
5338 get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5339 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5340 return Err(KclError::refactor(format!(
5341 "Could not resolve solid reference for selected cap: artifact_id={:?}",
5342 on_object.artifact_id
5343 )));
5344 };
5345 let solid_expr = indexed_solid_expr_for_sweep_output(
5346 ast_name_expr(solid_name_expr.name.name.clone()),
5347 cap.solid_output_index,
5348 );
5349 let face_expr = match cap.kind {
5351 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5352 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5353 };
5354
5355 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5356 }
5357 _ => Ok(None),
5358 }
5359}
5360
5361fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5362 match solid_output_index {
5363 Some(output_index) => create_index_expression(solid_expr, output_index),
5364 None => solid_expr,
5365 }
5366}
5367
5368fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5369 SourceRef::Simple {
5370 range: source.range,
5371 node_path: source.node_path.clone(),
5372 }
5373}
5374
5375fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5376 let source_ref = source_ref_from_source_ref_range(path_source);
5377 let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5378 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5379 return None;
5380 };
5381 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5382 return None;
5383 };
5384 if region_call.callee.name.name != "region" {
5385 return None;
5386 }
5387 Some(candidate)
5388}
5389
5390fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5391 let mut current_id = source_id;
5392 let mut current_composite = None;
5393 let mut visited = HashSet::new();
5394
5395 while visited.insert(current_id) {
5396 let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5397
5398 let Some(composite_id) = next_composite_id else {
5399 break;
5400 };
5401 let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5402 break;
5403 };
5404
5405 current_id = composite.id;
5406 current_composite = Some(composite);
5407
5408 if !composite.consumed {
5409 break;
5410 }
5411 }
5412
5413 current_composite.map(|composite| &composite.code_ref)
5414}
5415
5416fn downstream_composite_id_for_solid_source(
5417 artifact_graph: &ArtifactGraph,
5418 source_id: ArtifactId,
5419) -> Option<ArtifactId> {
5420 if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5422 && let Some(composite_id) = path.composite_solid_id
5423 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5424 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5425 {
5426 return Some(composite_id);
5427 }
5428
5429 for artifact in artifact_graph.values() {
5431 if let Artifact::Path(path) = artifact
5432 && path.sweep_id == Some(source_id)
5433 && let Some(composite_id) = path.composite_solid_id
5434 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5435 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5436 {
5437 return Some(composite_id);
5438 }
5439 }
5440
5441 artifact_graph.values().find_map(|artifact| {
5443 let Artifact::CompositeSolid(composite) = artifact else {
5444 return None;
5445 };
5446 composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5447 })
5448}
5449
5450fn composite_contains_path_input(
5451 solid_ids: &[ArtifactId],
5452 tool_ids: &[ArtifactId],
5453 path_id: ArtifactId,
5454 solid2d_id: Option<ArtifactId>,
5455) -> bool {
5456 composite_contains_input(solid_ids, tool_ids, path_id)
5457 || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5458}
5459
5460fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5461 solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5462}
5463
5464fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5465 let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5466 SourceRefRange {
5467 range: code_ref.range,
5468 node_path,
5469 }
5470}
5471
5472fn solid_output_index_for_sweep(
5473 artifact_graph: &ArtifactGraph,
5474 sweep_id: ArtifactId,
5475 sweep_code_ref: &CodeRef,
5476) -> Option<usize> {
5477 if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5480 return None;
5481 }
5482
5483 let sibling_sweeps = artifact_graph
5484 .values()
5485 .filter_map(|artifact| match artifact {
5486 Artifact::Sweep(sweep)
5487 if sweep.code_ref.range == sweep_code_ref.range
5488 && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5489 {
5490 Some(sweep)
5491 }
5492 _ => None,
5493 })
5494 .collect::<Vec<_>>();
5495
5496 if sibling_sweeps.len() <= 1 {
5497 return None;
5498 }
5499
5500 sibling_sweeps
5501 .iter()
5502 .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5503}
5504
5505fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5506 let mut existing_artifact_ids = scene_objects
5507 .iter()
5508 .map(|object| object.artifact_id)
5509 .collect::<HashSet<_>>();
5510
5511 for artifact in artifact_graph.values() {
5512 match artifact {
5513 Artifact::Wall(wall) => {
5514 if existing_artifact_ids.contains(&wall.id) {
5515 continue;
5516 }
5517
5518 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5519 Artifact::Segment(segment) => Some(segment),
5520 _ => None,
5521 }) else {
5522 continue;
5523 };
5524 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5525 Artifact::Sweep(sweep) => Some(sweep),
5526 _ => None,
5527 }) else {
5528 continue;
5529 };
5530 let source_segment = segment
5531 .original_seg_id
5532 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5533 .and_then(|artifact| match artifact {
5534 Artifact::Segment(segment) => Some(segment),
5535 _ => None,
5536 })
5537 .unwrap_or(segment);
5538 let solid_code_ref =
5539 downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5540 let path_code_ref = artifact_graph
5541 .get(&segment.path_id)
5542 .or_else(|| artifact_graph.get(&sweep.path_id))
5543 .and_then(|artifact| match artifact {
5544 Artifact::Path(path) => Some(&path.code_ref),
5545 _ => None,
5546 });
5547 let source = WallSource {
5548 solid: code_ref_source_ref_range(solid_code_ref),
5549 sweep: code_ref_source_ref_range(&sweep.code_ref),
5550 path: path_code_ref.map(code_ref_source_ref_range),
5551 segment: code_ref_source_ref_range(&source_segment.code_ref),
5552 };
5553 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5554 && solid_code_ref.node_path == sweep.code_ref.node_path)
5555 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5556 .flatten();
5557 let object_source = source_ref_from_source_ref_range(&source.solid);
5558 let id = ObjectId(scene_objects.len());
5559 scene_objects.push(crate::front::Object {
5560 id,
5561 kind: ObjectKind::Wall(crate::frontend::api::Wall {
5562 id,
5563 source,
5564 solid_output_index,
5565 }),
5566 label: Default::default(),
5567 comments: Default::default(),
5568 artifact_id: wall.id,
5569 source: object_source,
5570 });
5571 existing_artifact_ids.insert(wall.id);
5572 }
5573 Artifact::Cap(cap) => {
5574 if existing_artifact_ids.contains(&cap.id) {
5575 continue;
5576 }
5577
5578 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5579 Artifact::Sweep(sweep) => Some(sweep),
5580 _ => None,
5581 }) else {
5582 continue;
5583 };
5584 let id = ObjectId(scene_objects.len());
5585 let kind = match cap.sub_type {
5586 CapSubType::Start => crate::frontend::api::CapKind::Start,
5587 CapSubType::End => crate::frontend::api::CapKind::End,
5588 };
5589 let solid_code_ref =
5590 downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5591 let source = CapSource {
5592 solid: code_ref_source_ref_range(solid_code_ref),
5593 sweep: code_ref_source_ref_range(&sweep.code_ref),
5594 };
5595 let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5596 && solid_code_ref.node_path == sweep.code_ref.node_path)
5597 .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5598 .flatten();
5599 let object_source = source_ref_from_source_ref_range(&source.solid);
5600 scene_objects.push(crate::front::Object {
5601 id,
5602 kind: ObjectKind::Cap(crate::frontend::api::Cap {
5603 id,
5604 kind,
5605 source,
5606 solid_output_index,
5607 }),
5608 label: Default::default(),
5609 comments: Default::default(),
5610 artifact_id: cap.id,
5611 source: object_source,
5612 });
5613 existing_artifact_ids.insert(cap.id);
5614 }
5615 _ => {}
5616 }
5617 }
5618}
5619
5620fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5621 use crate::engine::PlaneName;
5622
5623 match name {
5624 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5625 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5626 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5627 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5628 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5629 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5630 }
5631}
5632
5633fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5634 ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5635 ast::UnaryOperator::Neg,
5636 ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5637 )))
5638}
5639
5640fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5641 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5642 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5643 unlabeled: Some(solid_expr),
5644 arguments: vec![ast::LabeledArg {
5645 label: Some(ast::Identifier::new("face")),
5646 arg: face_expr,
5647 }],
5648 digest: None,
5649 non_code_meta: Default::default(),
5650 })))
5651}
5652
5653fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5654 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5655 callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5656 unlabeled: Some(solid_expr),
5657 arguments: vec![ast::LabeledArg {
5658 label: Some(ast::Identifier::new("index")),
5659 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5660 ast::NumericLiteral {
5661 value: index as f64,
5662 suffix: NumericSuffix::None,
5663 raw: index.to_string(),
5664 digest: None,
5665 },
5666 )))),
5667 }],
5668 digest: None,
5669 non_code_meta: Default::default(),
5670 })))
5671}
5672
5673fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5674 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5675 return None;
5676 };
5677 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5678 return None;
5679 };
5680 if !matches!(
5681 sweep_call.callee.name.name.as_str(),
5682 "extrude" | "revolve" | "sweep" | "loft"
5683 ) {
5684 return None;
5685 }
5686 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5687 return None;
5688 };
5689 let candidate = region_name_expr.name.name.clone();
5690 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5691 return None;
5692 };
5693 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5694 return None;
5695 };
5696 if region_call.callee.name.name != "region" {
5697 return None;
5698 }
5699 Some(candidate)
5700}
5701
5702fn get_or_insert_ast_reference(
5709 ast: &mut ast::Node<ast::Program>,
5710 source_ref: &SourceRef,
5711 prefix: &str,
5712 property: Option<&str>,
5713) -> Result<ast::Expr, KclError> {
5714 let command = AstMutateCommand::AddVariableDeclaration {
5715 prefix: prefix.to_owned(),
5716 };
5717 let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5718 Ok((_, ret)) => ret,
5719 Err(err) => {
5720 if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5721 AstMutateCommandReturn::Name(var_name)
5722 } else {
5723 return Err(err);
5724 }
5725 }
5726 };
5727 let AstMutateCommandReturn::Name(var_name) = ret else {
5728 return Err(KclError::refactor(
5729 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5730 ));
5731 };
5732 let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5733 let Some(property) = property else {
5734 return Ok(var_expr);
5736 };
5737
5738 Ok(create_member_expression(var_expr, property))
5739}
5740
5741fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5742 let source_range = match source_ref {
5743 SourceRef::Simple { range, .. } => *range,
5744 SourceRef::BackTrace { ranges } => {
5745 let [range] = ranges.as_slice() else {
5746 return None;
5747 };
5748 range.0
5749 }
5750 };
5751 ast.body.iter().find_map(|item| {
5752 let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5753 return None;
5754 };
5755 let init_range = SourceRange::from(&var_decl.declaration.init);
5756 let source_is_inside_init = init_range.module_id() == source_range.module_id()
5757 && init_range.start() <= source_range.start()
5758 && source_range.end() <= init_range.end();
5759 if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5760 && init_range != source_range
5761 && source_is_inside_init
5762 {
5763 return None;
5764 }
5765 source_is_inside_init.then(|| var_decl.name().to_owned())
5766 })
5767}
5768
5769fn mutate_ast_node_by_source_ref(
5770 ast: &mut ast::Node<ast::Program>,
5771 source_ref: &SourceRef,
5772 command: AstMutateCommand,
5773) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5774 let (source_range, node_path) = match source_ref {
5775 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5776 SourceRef::BackTrace { ranges } => {
5777 let [range] = ranges.as_slice() else {
5778 return Err(KclError::refactor(format!(
5779 "Expected single source ref, got {}; ranges={ranges:#?}",
5780 ranges.len(),
5781 )));
5782 };
5783 (range.0, range.1.clone())
5784 }
5785 };
5786 let mut context = AstMutateContext {
5787 source_range,
5788 node_path,
5789 command,
5790 defined_names_stack: Default::default(),
5791 };
5792 let control = dfs_mut(ast, &mut context);
5793 match control {
5794 ControlFlow::Continue(_) => Err(KclError::refactor(
5795 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5796 )),
5797 ControlFlow::Break(break_value) => break_value,
5798 }
5799}
5800
5801#[derive(Debug)]
5802struct AstMutateContext {
5803 source_range: SourceRange,
5804 node_path: Option<ast::NodePath>,
5805 command: AstMutateCommand,
5806 defined_names_stack: Vec<HashSet<String>>,
5807}
5808
5809#[derive(Debug)]
5810#[allow(clippy::large_enum_variant)]
5811enum AstMutateCommand {
5812 AddSketchBlockExprStmt {
5814 expr: ast::Expr,
5815 },
5816 AddSketchBlockVarDecl {
5818 prefix: String,
5819 expr: ast::Expr,
5820 },
5821 AddVariableDeclaration {
5822 prefix: String,
5823 },
5824 EditPoint {
5825 at: ast::Expr,
5826 },
5827 EditLine {
5828 start: ast::Expr,
5829 end: ast::Expr,
5830 construction: Option<bool>,
5831 },
5832 EditArc {
5833 start: ast::Expr,
5834 end: ast::Expr,
5835 center: ast::Expr,
5836 direction: Option<ArcDirection>,
5837 construction: Option<bool>,
5838 },
5839 EditCircle {
5840 start: ast::Expr,
5841 center: ast::Expr,
5842 construction: Option<bool>,
5843 },
5844 EditControlPointSpline {
5845 points: ast::Expr,
5846 construction: Option<bool>,
5847 },
5848 EditConstraintValue {
5849 value: ast::BinaryPart,
5850 },
5851 EditAngleConstraint {
5852 call: ast::BinaryPart,
5853 value: ast::BinaryPart,
5854 },
5855 EditDistanceConstraint {
5856 call: ast::BinaryPart,
5857 value: ast::BinaryPart,
5858 },
5859 EditDistanceConstraintLabelPosition {
5860 label_position: ast::Expr,
5861 },
5862 EditCallUnlabeled {
5863 arg: ast::Expr,
5864 },
5865 EditVarInitialValue {
5866 value: Number,
5867 },
5868 DeleteNode,
5869}
5870
5871impl AstMutateCommand {
5872 fn needs_defined_names_stack(&self) -> bool {
5873 matches!(
5874 self,
5875 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5876 )
5877 }
5878}
5879
5880#[derive(Debug)]
5881enum AstMutateCommandReturn {
5882 None,
5883 Name(String),
5884}
5885
5886#[derive(Debug, Clone)]
5887struct AstNodeRef {
5888 range: SourceRange,
5889 node_path: Option<ast::NodePath>,
5890}
5891
5892impl<T> From<&ast::Node<T>> for AstNodeRef {
5893 fn from(value: &ast::Node<T>) -> Self {
5894 AstNodeRef {
5895 range: value.into(),
5896 node_path: value.node_path.clone(),
5897 }
5898 }
5899}
5900
5901impl From<&ast::BodyItem> for AstNodeRef {
5902 fn from(value: &ast::BodyItem) -> Self {
5903 match value {
5904 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5905 range: node.into(),
5906 node_path: node.node_path.clone(),
5907 },
5908 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5909 range: node.into(),
5910 node_path: node.node_path.clone(),
5911 },
5912 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5913 range: node.into(),
5914 node_path: node.node_path.clone(),
5915 },
5916 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5917 range: node.into(),
5918 node_path: node.node_path.clone(),
5919 },
5920 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5921 range: node.into(),
5922 node_path: node.node_path.clone(),
5923 },
5924 }
5925 }
5926}
5927
5928impl From<&ast::Expr> for AstNodeRef {
5929 fn from(value: &ast::Expr) -> Self {
5930 AstNodeRef {
5931 range: SourceRange::from(value),
5932 node_path: value.node_path().cloned(),
5933 }
5934 }
5935}
5936
5937impl From<&AstMutateContext> for AstNodeRef {
5938 fn from(value: &AstMutateContext) -> Self {
5939 AstNodeRef {
5940 range: value.source_range,
5941 node_path: value.node_path.clone(),
5942 }
5943 }
5944}
5945
5946impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5947 type Error = crate::walk::AstNodeError;
5948
5949 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5950 Ok(AstNodeRef {
5951 range: SourceRange::try_from(value)?,
5952 node_path: value.try_into()?,
5953 })
5954 }
5955}
5956
5957impl From<AstNodeRef> for SourceRange {
5958 fn from(value: AstNodeRef) -> Self {
5959 value.range
5960 }
5961}
5962
5963impl Visitor for AstMutateContext {
5964 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5965 type Continue = ();
5966
5967 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5968 filter_and_process(self, node)
5969 }
5970
5971 fn finish(&mut self, node: NodeMut<'_>) {
5972 match &node {
5973 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5974 self.defined_names_stack.pop();
5975 }
5976 _ => {}
5977 }
5978 }
5979}
5980
5981fn filter_and_process(
5982 ctx: &mut AstMutateContext,
5983 node: NodeMut,
5984) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5985 let Ok(node_range) = SourceRange::try_from(&node) else {
5986 return TraversalReturn::new_continue(());
5988 };
5989 if let NodeMut::VariableDeclaration(var_decl) = &node {
5994 let expr_range = SourceRange::from(&var_decl.declaration.init);
5995 let expr_node_path = var_decl.declaration.init.node_path();
5996 if source_ref_matches(ctx, expr_range, expr_node_path) {
5997 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5998 return TraversalReturn::new_break(Ok((
6001 AstNodeRef::from(&**var_decl),
6002 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6003 )));
6004 }
6005 if let AstMutateCommand::DeleteNode = &ctx.command {
6006 return TraversalReturn {
6009 mutate_body_item: MutateBodyItem::Delete,
6010 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6011 };
6012 }
6013 }
6014 }
6015 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6018 let expr_range = SourceRange::from(&expr_stmt.expression);
6019 let expr_node_path = expr_stmt.expression.node_path();
6020 if source_ref_matches(ctx, expr_range, expr_node_path) {
6021 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6022 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6025 return TraversalReturn::new_continue(());
6026 };
6027 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6028 }
6029 if let AstMutateCommand::DeleteNode = &ctx.command {
6030 return TraversalReturn {
6033 mutate_body_item: MutateBodyItem::Delete,
6034 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6035 };
6036 }
6037 }
6038 }
6039
6040 if ctx.command.needs_defined_names_stack() {
6041 if let NodeMut::Program(program) = &node {
6042 ctx.defined_names_stack.push(find_defined_names(*program));
6043 } else if let NodeMut::SketchBlock(block) = &node {
6044 ctx.defined_names_stack.push(find_defined_names(&block.body));
6045 }
6046 }
6047
6048 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6050 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6051 return TraversalReturn::new_continue(());
6052 }
6053 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6054 return TraversalReturn::new_continue(());
6055 };
6056 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6057}
6058
6059fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6060 match &ctx.node_path {
6061 Some(target) => Some(target) == node_path,
6062 None => node_range == ctx.source_range,
6063 }
6064}
6065
6066fn is_angle_constraint_call_name(name: &str) -> bool {
6067 matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6068}
6069
6070fn is_distance_constraint_call_name(name: &str) -> bool {
6071 matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6072}
6073
6074fn is_constraint_call_name(name: &str) -> bool {
6075 matches!(
6076 name,
6077 DISTANCE_FN
6078 | HORIZONTAL_DISTANCE_FN
6079 | VERTICAL_DISTANCE_FN
6080 | RADIUS_FN
6081 | DIAMETER_FN
6082 | ANGLE_FN
6083 | ANGLE_DIMENSION_FN
6084 )
6085}
6086
6087fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6088 if let ast::BinaryPart::CallExpressionKw(call) = part
6089 && is_constraint_call_name(call.callee.name.name.as_str())
6090 {
6091 Some(call)
6092 } else {
6093 None
6094 }
6095}
6096
6097fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6098 match &ctx.command {
6099 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6100 if let NodeMut::SketchBlock(sketch_block) = node {
6101 sketch_block
6102 .body
6103 .items
6104 .push(ast::BodyItem::ExpressionStatement(ast::Node {
6105 inner: ast::ExpressionStatement {
6106 expression: expr.clone(),
6107 digest: None,
6108 },
6109 start: Default::default(),
6110 end: Default::default(),
6111 module_id: Default::default(),
6112 node_path: None,
6113 outer_attrs: Default::default(),
6114 pre_comments: Default::default(),
6115 comment_start: Default::default(),
6116 }));
6117 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6118 }
6119 }
6120 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6121 if let NodeMut::SketchBlock(sketch_block) = node {
6122 let empty_defined_names = HashSet::new();
6123 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6124 let Ok(name) = next_free_name(prefix, defined_names) else {
6125 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6126 };
6127 sketch_block
6128 .body
6129 .items
6130 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6131 ast::VariableDeclaration::new(
6132 ast::VariableDeclarator::new(&name, expr.clone()),
6133 ast::ItemVisibility::Default,
6134 ast::VariableKind::Const,
6135 ),
6136 ))));
6137 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6138 }
6139 }
6140 AstMutateCommand::AddVariableDeclaration { prefix } => {
6141 if let NodeMut::VariableDeclaration(inner) = node {
6142 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6143 }
6144 if let NodeMut::ExpressionStatement(expr_stmt) = node {
6145 let empty_defined_names = HashSet::new();
6146 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6147 let Ok(name) = next_free_name(prefix, defined_names) else {
6148 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6150 };
6151 let mutate_node =
6152 ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6153 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6154 ast::ItemVisibility::Default,
6155 ast::VariableKind::Const,
6156 ))));
6157 return TraversalReturn {
6158 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6159 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6160 };
6161 }
6162 }
6163 AstMutateCommand::EditPoint { at } => {
6164 if let NodeMut::CallExpressionKw(call) = node {
6165 if call.callee.name.name != POINT_FN {
6166 return TraversalReturn::new_continue(());
6167 }
6168 for labeled_arg in &mut call.arguments {
6170 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6171 labeled_arg.arg = at.clone();
6172 }
6173 }
6174 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6175 }
6176 }
6177 AstMutateCommand::EditLine {
6178 start,
6179 end,
6180 construction,
6181 } => {
6182 if let NodeMut::CallExpressionKw(call) = node {
6183 if call.callee.name.name != LINE_FN {
6184 return TraversalReturn::new_continue(());
6185 }
6186 for labeled_arg in &mut call.arguments {
6188 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6189 labeled_arg.arg = start.clone();
6190 }
6191 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6192 labeled_arg.arg = end.clone();
6193 }
6194 }
6195 if let Some(construction_value) = construction {
6197 let construction_exists = call
6198 .arguments
6199 .iter()
6200 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6201 if *construction_value {
6202 if construction_exists {
6204 for labeled_arg in &mut call.arguments {
6206 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6207 labeled_arg.arg =
6208 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6209 value: ast::LiteralValue::Bool(true),
6210 raw: "true".to_string(),
6211 digest: None,
6212 })));
6213 }
6214 }
6215 } else {
6216 call.arguments.push(ast::LabeledArg {
6218 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6219 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6220 value: ast::LiteralValue::Bool(true),
6221 raw: "true".to_string(),
6222 digest: None,
6223 }))),
6224 });
6225 }
6226 } else {
6227 call.arguments
6229 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6230 }
6231 }
6232 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6233 }
6234 }
6235 AstMutateCommand::EditArc {
6236 start,
6237 end,
6238 center,
6239 direction,
6240 construction,
6241 } => {
6242 if let NodeMut::CallExpressionKw(call) = node {
6243 if call.callee.name.name != ARC_FN {
6244 return TraversalReturn::new_continue(());
6245 }
6246 for labeled_arg in &mut call.arguments {
6248 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6249 labeled_arg.arg = start.clone();
6250 }
6251 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6252 labeled_arg.arg = end.clone();
6253 }
6254 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6255 labeled_arg.arg = center.clone();
6256 }
6257 }
6258 if let Some(direction_value) = direction {
6260 let direction_exists = call
6261 .arguments
6262 .iter()
6263 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6264 if direction_value.is_clockwise() {
6265 let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6266 if direction_exists {
6267 for labeled_arg in &mut call.arguments {
6269 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6270 labeled_arg.arg = direction_ast.clone();
6271 }
6272 }
6273 } else {
6274 call.arguments.push(ast::LabeledArg {
6276 label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6277 arg: direction_ast,
6278 });
6279 }
6280 } else {
6281 call.arguments
6284 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6285 }
6286 }
6287 if let Some(construction_value) = construction {
6289 let construction_exists = call
6290 .arguments
6291 .iter()
6292 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6293 if *construction_value {
6294 if construction_exists {
6296 for labeled_arg in &mut call.arguments {
6298 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6299 labeled_arg.arg =
6300 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6301 value: ast::LiteralValue::Bool(true),
6302 raw: "true".to_string(),
6303 digest: None,
6304 })));
6305 }
6306 }
6307 } else {
6308 call.arguments.push(ast::LabeledArg {
6310 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6311 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6312 value: ast::LiteralValue::Bool(true),
6313 raw: "true".to_string(),
6314 digest: None,
6315 }))),
6316 });
6317 }
6318 } else {
6319 call.arguments
6321 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6322 }
6323 }
6324 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6325 }
6326 }
6327 AstMutateCommand::EditCircle {
6328 start,
6329 center,
6330 construction,
6331 } => {
6332 if let NodeMut::CallExpressionKw(call) = node {
6333 if call.callee.name.name != CIRCLE_FN {
6334 return TraversalReturn::new_continue(());
6335 }
6336 for labeled_arg in &mut call.arguments {
6338 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6339 labeled_arg.arg = start.clone();
6340 }
6341 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6342 labeled_arg.arg = center.clone();
6343 }
6344 }
6345 if let Some(construction_value) = construction {
6347 let construction_exists = call
6348 .arguments
6349 .iter()
6350 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6351 if *construction_value {
6352 if construction_exists {
6353 for labeled_arg in &mut call.arguments {
6355 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6356 labeled_arg.arg =
6357 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6358 value: ast::LiteralValue::Bool(true),
6359 raw: "true".to_string(),
6360 digest: None,
6361 })));
6362 }
6363 }
6364 } else {
6365 call.arguments.push(ast::LabeledArg {
6367 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6368 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6369 value: ast::LiteralValue::Bool(true),
6370 raw: "true".to_string(),
6371 digest: None,
6372 }))),
6373 });
6374 }
6375 } else {
6376 call.arguments
6378 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6379 }
6380 }
6381 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6382 }
6383 }
6384 AstMutateCommand::EditControlPointSpline { points, construction } => {
6385 if let NodeMut::CallExpressionKw(call) = node {
6386 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6387 return TraversalReturn::new_continue(());
6388 }
6389 for labeled_arg in &mut call.arguments {
6390 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6391 {
6392 labeled_arg.arg = points.clone();
6393 }
6394 }
6395 if let Some(construction_value) = construction {
6397 let construction_exists = call
6398 .arguments
6399 .iter()
6400 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6401 if *construction_value {
6402 if construction_exists {
6403 for labeled_arg in &mut call.arguments {
6404 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6405 labeled_arg.arg =
6406 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6407 value: ast::LiteralValue::Bool(true),
6408 raw: "true".to_string(),
6409 digest: None,
6410 })));
6411 }
6412 }
6413 } else {
6414 call.arguments.push(ast::LabeledArg {
6415 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6416 arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6417 value: ast::LiteralValue::Bool(true),
6418 raw: "true".to_string(),
6419 digest: None,
6420 }))),
6421 });
6422 }
6423 } else {
6424 call.arguments
6425 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6426 }
6427 }
6428 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6429 }
6430 }
6431 AstMutateCommand::EditConstraintValue { value } => {
6432 if let NodeMut::BinaryExpression(binary_expr) = node {
6433 let left_is_constraint = matches!(
6434 &binary_expr.left,
6435 ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6436 );
6437 if left_is_constraint {
6438 binary_expr.right = value.clone();
6439 } else {
6440 binary_expr.left = value.clone();
6441 }
6442
6443 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6444 }
6445 }
6446 AstMutateCommand::EditAngleConstraint { call, value } => {
6447 if let NodeMut::BinaryExpression(binary_expr) = node {
6448 let left_is_angle = matches!(
6449 &binary_expr.left,
6450 ast::BinaryPart::CallExpressionKw(existing_call)
6451 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6452 );
6453 let right_is_angle = matches!(
6454 &binary_expr.right,
6455 ast::BinaryPart::CallExpressionKw(existing_call)
6456 if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6457 );
6458
6459 match (left_is_angle, right_is_angle) {
6460 (true, _) => {
6461 binary_expr.left = call.clone();
6462 binary_expr.right = value.clone();
6463 }
6464 (false, true) => {
6465 binary_expr.left = value.clone();
6466 binary_expr.right = call.clone();
6467 }
6468 (false, false) => return TraversalReturn::new_continue(()),
6469 }
6470
6471 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6472 }
6473 }
6474 AstMutateCommand::EditDistanceConstraint { call, value } => {
6475 if let NodeMut::BinaryExpression(binary_expr) = node {
6476 let left_is_distance = matches!(
6477 &binary_expr.left,
6478 ast::BinaryPart::CallExpressionKw(existing_call)
6479 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6480 );
6481 let right_is_distance = matches!(
6482 &binary_expr.right,
6483 ast::BinaryPart::CallExpressionKw(existing_call)
6484 if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6485 );
6486
6487 match (left_is_distance, right_is_distance) {
6488 (true, _) => {
6489 binary_expr.left = call.clone();
6490 binary_expr.right = value.clone();
6491 }
6492 (false, true) => {
6493 binary_expr.left = value.clone();
6494 binary_expr.right = call.clone();
6495 }
6496 (false, false) => return TraversalReturn::new_continue(()),
6497 }
6498
6499 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6500 }
6501 }
6502 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6503 if let NodeMut::BinaryExpression(binary_expr) = node {
6504 let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6505 call
6506 } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6507 call
6508 } else {
6509 return TraversalReturn::new_continue(());
6510 };
6511
6512 if let Some(label_arg) = call
6513 .arguments
6514 .iter_mut()
6515 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6516 {
6517 label_arg.arg = label_position.clone();
6518 } else {
6519 call.arguments.push(ast::LabeledArg {
6520 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6521 arg: label_position.clone(),
6522 });
6523 }
6524
6525 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6526 }
6527 }
6528 AstMutateCommand::EditCallUnlabeled { arg } => {
6529 if let NodeMut::CallExpressionKw(call) = node {
6530 call.unlabeled = Some(arg.clone());
6531 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6532 }
6533 }
6534 AstMutateCommand::EditVarInitialValue { value } => {
6535 if let NodeMut::SketchVar(sketch_var) = node {
6539 let Ok(literal) = to_source_number(*value) else {
6540 return TraversalReturn::new_break(Err(KclError::refactor(format!(
6541 "Could not convert number to AST literal: {:?}",
6542 *value
6543 ))));
6544 };
6545 sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6546 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6547 }
6548 }
6549 AstMutateCommand::DeleteNode => {
6550 return TraversalReturn {
6551 mutate_body_item: MutateBodyItem::Delete,
6552 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6553 };
6554 }
6555 }
6556 TraversalReturn::new_continue(())
6557}
6558
6559struct FindSketchBlockSourceRange {
6560 target_before_mutation: SourceRange,
6562 found: Cell<Option<AstNodeRef>>,
6566}
6567
6568impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6569 type Error = crate::front::Error;
6570
6571 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6572 let Ok(node_range) = SourceRange::try_from(&node) else {
6573 return Ok(true);
6574 };
6575
6576 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6577 if node_range.module_id() == self.target_before_mutation.module_id()
6578 && node_range.start() == self.target_before_mutation.start()
6579 && node_range.end() >= self.target_before_mutation.end()
6581 {
6582 self.found.set(sketch_block.body.items.last().map(|item| match item {
6583 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6587 _ => AstNodeRef::from(item),
6588 }));
6589 return Ok(false);
6590 } else {
6591 return Ok(true);
6594 }
6595 }
6596
6597 for child in node.children().iter() {
6598 if !child.visit(*self)? {
6599 return Ok(false);
6600 }
6601 }
6602
6603 Ok(true)
6604 }
6605}
6606
6607struct FindSketchBlockByNodePath {
6608 target_node_path: ast::NodePath,
6610 found: Cell<Option<AstNodeRef>>,
6614}
6615
6616impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6617 type Error = crate::front::Error;
6618
6619 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6620 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6621 return Ok(true);
6622 };
6623
6624 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6625 if let Some(node_path) = node_path
6626 && node_path == self.target_node_path
6627 {
6628 self.found.set(sketch_block.body.items.last().map(|item| match item {
6629 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6633 _ => AstNodeRef::from(item),
6634 }));
6635
6636 return Ok(false);
6637 } else {
6638 return Ok(true);
6641 }
6642 }
6643
6644 for child in node.children().iter() {
6645 if !child.visit(*self)? {
6646 return Ok(false);
6647 }
6648 }
6649
6650 Ok(true)
6651 }
6652}
6653
6654fn find_sketch_block_added_item(
6662 ast: &ast::Node<ast::Program>,
6663 sketch_block_before_mutation: &AstNodeRef,
6664) -> Result<AstNodeRef, KclError> {
6665 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6666 let find = FindSketchBlockByNodePath {
6667 target_node_path: node_path.clone(),
6668 found: Cell::new(None),
6669 };
6670 let node = crate::walk::Node::from(ast);
6671 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6672 find.found.into_inner().ok_or_else(|| {
6673 KclError::refactor(format!(
6674 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6675 ))
6676 })
6677 } else {
6678 let find = FindSketchBlockSourceRange {
6680 target_before_mutation: sketch_block_before_mutation.range,
6681 found: Cell::new(None),
6682 };
6683 let node = crate::walk::Node::from(ast);
6684 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6685 find.found.into_inner().ok_or_else(|| KclError::refactor(
6686 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?"),
6687 ))
6688 }
6689}
6690
6691fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6692 let message = error.message().trim();
6693 let message = if message.is_empty() {
6694 "unknown parse error"
6695 } else {
6696 message
6697 };
6698
6699 format!("{prefix}: {message}")
6700}
6701
6702fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6703 let (program, errors) = Program::parse(source).map_err(|err| {
6704 KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6705 })?;
6706 if !errors.is_empty() {
6707 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6708 format_compilation_issues(parse_error_prefix, &errors),
6709 )));
6710 }
6711
6712 program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6713}
6714
6715fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6716 let Some(first_issue) = issues
6717 .iter()
6718 .find(|issue| issue.severity.is_err())
6719 .or_else(|| issues.first())
6720 else {
6721 return prefix.to_owned();
6722 };
6723
6724 let message = first_issue.message.trim();
6725 let message = if message.is_empty() {
6726 "unknown parse error"
6727 } else {
6728 message
6729 };
6730
6731 if issues.len() > 1 {
6732 format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6733 } else {
6734 format!("{prefix}: {message}")
6735 }
6736}
6737
6738fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6739 ast.recast_top(&Default::default(), 0)
6741}
6742
6743struct FindNumericLiteral {
6744 target: SourceRange,
6745 found: Cell<Option<ast::NumericLiteral>>,
6746}
6747
6748impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6749 type Error = crate::front::Error;
6750
6751 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6752 let Ok(node_range) = SourceRange::try_from(&node) else {
6753 return Ok(true);
6754 };
6755
6756 if node_range == self.target
6757 && let crate::walk::Node::NumericLiteral(literal) = node
6758 {
6759 self.found.set(Some(literal.inner.clone()));
6760 return Ok(false);
6761 }
6762
6763 for child in node.children().iter() {
6764 if !child.visit(*self)? {
6765 return Ok(false);
6766 }
6767 }
6768
6769 Ok(true)
6770 }
6771}
6772
6773fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6774 let find = FindNumericLiteral {
6775 target,
6776 found: Cell::new(None),
6777 };
6778 let node = crate::walk::Node::from(ast);
6779 node.visit(&find).ok()?;
6780 find.found.into_inner()
6781}
6782
6783struct FindSketchVarInitialByNodePath<'a> {
6784 target: &'a ast::NodePath,
6785 sketch_var_found: Cell<bool>,
6786 initial_literal: Cell<Option<ast::NumericLiteral>>,
6787}
6788
6789impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6790 type Error = crate::front::Error;
6791
6792 fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6793 if let crate::walk::Node::SketchVar(sketch_var) = node
6794 && sketch_var.node_path.as_ref() == Some(self.target)
6795 {
6796 self.sketch_var_found.set(true);
6797 if let Some(initial) = &sketch_var.initial {
6798 self.initial_literal.set(Some(initial.inner.clone()));
6799 }
6800 return Ok(false);
6801 }
6802
6803 for child in node.children().iter() {
6804 if !child.visit(*self)? {
6805 return Ok(false);
6806 }
6807 }
6808
6809 Ok(true)
6810 }
6811}
6812
6813fn numeric_literal_at_node_path(
6823 ast: &ast::Node<ast::Program>,
6824 node_path: Option<&ast::NodePath>,
6825 source_range: SourceRange,
6826) -> Option<Option<ast::NumericLiteral>> {
6827 let Some(node_path) = node_path else {
6828 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";
6829 #[cfg(target_arch = "wasm32")]
6830 web_sys::console::warn_1(&message.into());
6831 #[cfg(not(target_arch = "wasm32"))]
6832 eprintln!("WARNING: {message}");
6833 return numeric_literal_at_source_range(ast, source_range).map(Some);
6834 };
6835 let find = FindSketchVarInitialByNodePath {
6836 target: node_path,
6837 sketch_var_found: Cell::new(false),
6838 initial_literal: Cell::new(None),
6839 };
6840 let node = crate::walk::Node::from(ast);
6841 node.visit(&find).ok()?;
6842 if !find.sketch_var_found.get() {
6843 return None;
6844 }
6845 Some(find.initial_literal.into_inner())
6846}
6847
6848fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6849 match suffix {
6850 NumericSuffix::Mm => Some(UnitLength::Millimeters),
6851 NumericSuffix::Cm => Some(UnitLength::Centimeters),
6852 NumericSuffix::M => Some(UnitLength::Meters),
6853 NumericSuffix::Inch => Some(UnitLength::Inches),
6854 NumericSuffix::Ft => Some(UnitLength::Feet),
6855 NumericSuffix::Yd => Some(UnitLength::Yards),
6856 _ => None,
6857 }
6858}
6859
6860fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6861 match suffix_length_unit(number.units) {
6862 Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6863 None => number.value,
6864 }
6865}
6866
6867fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6868 match suffix_length_unit(literal.suffix) {
6869 Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6870 None => literal.value,
6871 }
6872}
6873
6874fn var_solution_needs_commit(
6875 current_literal: &ast::NumericLiteral,
6876 solved_value: Number,
6877 default_length_unit: UnitLength,
6878) -> bool {
6879 let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6880 let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6881
6882 (current - solved).abs() > 1e-9
6883}
6884
6885fn preserve_var_solution_literal_style(
6886 current_literal: &ast::NumericLiteral,
6887 solved_value: Number,
6888 default_length_unit: UnitLength,
6889) -> Number {
6890 if current_literal.suffix == NumericSuffix::None {
6891 return Number {
6892 value: number_value_in_default_length_units(solved_value, default_length_unit),
6893 units: NumericSuffix::None,
6894 };
6895 }
6896
6897 let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6898 return solved_value;
6899 };
6900
6901 let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6902 Number {
6903 value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6904 units: current_literal.suffix,
6905 }
6906}
6907
6908pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6909 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6910 inner: ast::ArrayExpression {
6911 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6912 non_code_meta: Default::default(),
6913 digest: None,
6914 },
6915 start: Default::default(),
6916 end: Default::default(),
6917 module_id: Default::default(),
6918 node_path: None,
6919 outer_attrs: Default::default(),
6920 pre_comments: Default::default(),
6921 comment_start: Default::default(),
6922 })))
6923}
6924
6925pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6926 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6927 ast::ArrayExpression {
6928 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6929 digest: None,
6930 non_code_meta: Default::default(),
6931 },
6932 ))))
6933}
6934
6935fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6936 Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6937 ast::ArrayExpression {
6938 elements: vec![
6939 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6940 point.x,
6941 )?)))),
6942 ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6943 point.y,
6944 )?)))),
6945 ],
6946 non_code_meta: Default::default(),
6947 digest: None,
6948 },
6949 ))))
6950}
6951
6952fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6953 match expr {
6954 Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6955 inner: ast::Literal::from(to_source_number(*number)?),
6956 start: Default::default(),
6957 end: Default::default(),
6958 module_id: Default::default(),
6959 node_path: None,
6960 outer_attrs: Default::default(),
6961 pre_comments: Default::default(),
6962 comment_start: Default::default(),
6963 }))),
6964 Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6965 inner: ast::SketchVar {
6966 initial: Some(BoxNode::new(ast::Node {
6967 inner: to_source_number(*number)?,
6968 start: Default::default(),
6969 end: Default::default(),
6970 module_id: Default::default(),
6971 node_path: None,
6972 outer_attrs: Default::default(),
6973 pre_comments: Default::default(),
6974 comment_start: Default::default(),
6975 })),
6976 digest: None,
6977 },
6978 start: Default::default(),
6979 end: Default::default(),
6980 module_id: Default::default(),
6981 node_path: None,
6982 outer_attrs: Default::default(),
6983 pre_comments: Default::default(),
6984 comment_start: Default::default(),
6985 }))),
6986 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6987 }
6988}
6989
6990fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6991 Ok(ast::NumericLiteral {
6992 value: number.value,
6993 suffix: number.units,
6994 raw: format_number_literal(number.value, number.units, None)?,
6995 digest: None,
6996 })
6997}
6998
6999pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7000 ast::Expr::Name(BoxNode::new(ast_name(name)))
7001}
7002
7003fn ast_name(name: String) -> ast::Node<ast::Name> {
7004 ast::Node {
7005 inner: ast::Name {
7006 name: ast::Node {
7007 inner: ast::Identifier { name, digest: None },
7008 start: Default::default(),
7009 end: Default::default(),
7010 module_id: Default::default(),
7011 node_path: None,
7012 outer_attrs: Default::default(),
7013 pre_comments: Default::default(),
7014 comment_start: Default::default(),
7015 },
7016 path: Vec::new(),
7017 abs_path: false,
7018 digest: None,
7019 },
7020 start: Default::default(),
7021 end: Default::default(),
7022 module_id: Default::default(),
7023 node_path: None,
7024 outer_attrs: Default::default(),
7025 pre_comments: Default::default(),
7026 comment_start: Default::default(),
7027 }
7028}
7029
7030pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7031 ast::Name {
7032 name: ast::Node {
7033 inner: ast::Identifier {
7034 name: name.to_owned(),
7035 digest: None,
7036 },
7037 start: Default::default(),
7038 end: Default::default(),
7039 module_id: Default::default(),
7040 node_path: None,
7041 outer_attrs: Default::default(),
7042 pre_comments: Default::default(),
7043 comment_start: Default::default(),
7044 },
7045 path: Default::default(),
7046 abs_path: false,
7047 digest: None,
7048 }
7049}
7050
7051pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
7055 let elements = exprs.into_iter().collect::<Vec<_>>();
7056 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
7057
7058 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7060 elements,
7061 digest: None,
7062 non_code_meta: Default::default(),
7063 })));
7064
7065 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7067 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
7068 unlabeled: Some(array_expr),
7069 arguments: Default::default(),
7070 digest: None,
7071 non_code_meta: Default::default(),
7072 })))
7073}
7074
7075pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
7077 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7078 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
7079 unlabeled: None,
7080 arguments: vec![
7081 ast::LabeledArg {
7082 label: Some(ast::Identifier::new(LINE_START_PARAM)),
7083 arg: start_ast,
7084 },
7085 ast::LabeledArg {
7086 label: Some(ast::Identifier::new(LINE_END_PARAM)),
7087 arg: end_ast,
7088 },
7089 ],
7090 digest: None,
7091 non_code_meta: Default::default(),
7092 })))
7093}
7094
7095pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
7097 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7098 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
7099 unlabeled: None,
7100 arguments: vec![
7101 ast::LabeledArg {
7102 label: Some(ast::Identifier::new(ARC_START_PARAM)),
7103 arg: start_ast,
7104 },
7105 ast::LabeledArg {
7106 label: Some(ast::Identifier::new(ARC_END_PARAM)),
7107 arg: end_ast,
7108 },
7109 ast::LabeledArg {
7110 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
7111 arg: center_ast,
7112 },
7113 ],
7114 digest: None,
7115 non_code_meta: Default::default(),
7116 })))
7117}
7118
7119pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
7121 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7122 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
7123 unlabeled: None,
7124 arguments: vec![
7125 ast::LabeledArg {
7126 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
7127 arg: start_ast,
7128 },
7129 ast::LabeledArg {
7130 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
7131 arg: center_ast,
7132 },
7133 ],
7134 digest: None,
7135 non_code_meta: Default::default(),
7136 })))
7137}
7138
7139pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7141 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7142 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7143 unlabeled: Some(line_expr),
7144 arguments: Default::default(),
7145 digest: None,
7146 non_code_meta: Default::default(),
7147 })))
7148}
7149
7150pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7152 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7153 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7154 unlabeled: Some(line_expr),
7155 arguments: Default::default(),
7156 digest: None,
7157 non_code_meta: Default::default(),
7158 })))
7159}
7160
7161pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7163 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7164 object: object_expr,
7165 property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7166 name: ast::Node::no_src(ast::Identifier {
7167 name: property.to_string(),
7168 digest: None,
7169 }),
7170 path: Vec::new(),
7171 abs_path: false,
7172 digest: None,
7173 }))),
7174 computed: false,
7175 digest: None,
7176 })))
7177}
7178
7179pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7180 ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7181 object: object_expr,
7182 property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7183 ast::NumericLiteral {
7184 value: index as f64,
7185 suffix: NumericSuffix::None,
7186 raw: index.to_string(),
7187 digest: None,
7188 },
7189 )))),
7190 computed: true,
7191 digest: None,
7192 })))
7193}
7194
7195fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7197 let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7199 position.x,
7200 )?))));
7201 let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7202 position.y,
7203 )?))));
7204 let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7205 elements: vec![x_literal, y_literal],
7206 digest: None,
7207 non_code_meta: Default::default(),
7208 })));
7209
7210 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7212 elements: vec![point_expr, point_array],
7213 digest: None,
7214 non_code_meta: Default::default(),
7215 })));
7216
7217 Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7219 ast::CallExpressionKw {
7220 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7221 unlabeled: Some(array_expr),
7222 arguments: Default::default(),
7223 digest: None,
7224 non_code_meta: Default::default(),
7225 },
7226 ))))
7227}
7228
7229pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7231 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7232 elements: line_exprs,
7233 digest: None,
7234 non_code_meta: Default::default(),
7235 })));
7236
7237 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7239 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7240 unlabeled: Some(array_expr),
7241 arguments: Default::default(),
7242 digest: None,
7243 non_code_meta: Default::default(),
7244 })))
7245}
7246
7247pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7249 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7250 elements: segment_exprs,
7251 digest: None,
7252 non_code_meta: Default::default(),
7253 })));
7254
7255 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7256 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7257 unlabeled: Some(array_expr),
7258 arguments: Default::default(),
7259 digest: None,
7260 non_code_meta: Default::default(),
7261 })))
7262}
7263
7264pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7266 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7267 elements: vec![seg1_expr, seg2_expr],
7268 digest: None,
7269 non_code_meta: Default::default(),
7270 })));
7271
7272 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7273 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7274 unlabeled: Some(array_expr),
7275 arguments: Default::default(),
7276 digest: None,
7277 non_code_meta: Default::default(),
7278 })))
7279}
7280
7281pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7283 let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7284 elements: input_exprs,
7285 digest: None,
7286 non_code_meta: Default::default(),
7287 })));
7288 let arguments = vec![ast::LabeledArg {
7289 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7290 arg: axis_expr,
7291 }];
7292
7293 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7294 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7295 unlabeled: Some(array_expr),
7296 arguments,
7297 digest: None,
7298 non_code_meta: Default::default(),
7299 })))
7300}
7301
7302pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7304 let arguments = vec![ast::LabeledArg {
7305 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7306 arg: point_expr,
7307 }];
7308
7309 ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7310 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7311 unlabeled: Some(segment_expr),
7312 arguments,
7313 digest: None,
7314 non_code_meta: Default::default(),
7315 })))
7316}
7317
7318fn issue_source_range(error: &KclError) -> SourceRange {
7323 let source_ranges = error.source_ranges();
7324 source_ranges
7325 .iter()
7326 .find(|range| range.is_top_level_module())
7327 .or_else(|| source_ranges.first())
7328 .copied()
7329 .unwrap_or_else(SourceRange::synthetic)
7330}
7331
7332#[cfg(test)]
7333mod tests {
7334 use std::sync;
7335
7336 use super::*;
7337 use crate::engine::PlaneName;
7338 use crate::engine::engine_manager::EngineManager;
7339 use crate::execution::cache::SketchModeState;
7340 use crate::execution::cache::clear_mem_cache;
7341 use crate::execution::cache::read_old_memory;
7342 use crate::execution::cache::write_old_memory;
7343 use crate::front::Distance;
7344 use crate::front::Fixed;
7345 use crate::front::FixedPoint;
7346 use crate::front::Midpoint;
7347 use crate::front::Object;
7348 use crate::front::Plane;
7349 use crate::front::Sketch;
7350 use crate::front::Tangent;
7351 use crate::frontend::sketch::Vertical;
7352 use crate::pretty::NumericSuffix;
7353
7354 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7355 for object in &scene_graph.objects {
7356 if let ObjectKind::Sketch(_) = &object.kind {
7357 return Some(object);
7358 }
7359 }
7360 None
7361 }
7362
7363 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7364 for object in &scene_graph.objects {
7365 if let ObjectKind::Face(_) = &object.kind {
7366 return Some(object);
7367 }
7368 }
7369 None
7370 }
7371
7372 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7373 for object in &scene_graph.objects {
7374 if matches!(&object.kind, ObjectKind::Wall(_)) {
7375 return Some(object.id);
7376 }
7377 }
7378 None
7379 }
7380
7381 fn find_cap_object_id_with_solid_output_index(
7382 scene_graph: &SceneGraph,
7383 cap_kind: crate::frontend::api::CapKind,
7384 solid_output_index: usize,
7385 ) -> Option<ObjectId> {
7386 for object in &scene_graph.objects {
7387 if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7388 {
7389 return Some(object.id);
7390 }
7391 }
7392 None
7393 }
7394
7395 #[test]
7396 fn issue_source_range_prefers_top_level_module() {
7397 use kcl_error::ModuleId;
7398
7399 let top = SourceRange::new(10, 20, ModuleId::default());
7400 let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7401
7402 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7405 "boom".to_owned(),
7406 vec![imported, top],
7407 ));
7408 assert_eq!(super::issue_source_range(&error), top);
7409
7410 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7412 assert_eq!(super::issue_source_range(&error), imported);
7413
7414 let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7416 assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7417 }
7418
7419 #[test]
7420 fn composite_constituent_sweeps_are_not_solid_outputs() {
7421 use kcl_api::artifact::ArtifactSweepMethod;
7422 use kcl_api::artifact::CompositeSolid;
7423 use kcl_api::artifact::CompositeSolidSubType;
7424 use kcl_api::artifact::Sweep;
7425 use kcl_api::artifact::SweepSubType;
7426
7427 let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7428 let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7429 let composite_id = ArtifactId::new(Uuid::new_v4());
7430 let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7431 let sweep = |id| {
7432 Artifact::Sweep(Sweep {
7433 id,
7434 sub_type: SweepSubType::Extrusion,
7435 path_id: ArtifactId::new(Uuid::new_v4()),
7436 surface_ids: Vec::new(),
7437 edge_ids: Vec::new(),
7438 code_ref: code_ref.clone(),
7439 source_sweep_id: None,
7440 trajectory_id: None,
7441 method: ArtifactSweepMethod::New,
7442 consumed: false,
7443 pattern_ids: Vec::new(),
7444 })
7445 };
7446 let mut artifacts = IndexMap::from([
7447 (first_sweep_id, sweep(first_sweep_id)),
7448 (second_sweep_id, sweep(second_sweep_id)),
7449 ]);
7450
7451 let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7452 assert_eq!(
7453 solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7454 Some(0)
7455 );
7456 assert_eq!(
7457 solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7458 Some(1)
7459 );
7460
7461 artifacts.insert(
7462 composite_id,
7463 Artifact::CompositeSolid(CompositeSolid {
7464 id: composite_id,
7465 consumed: false,
7466 sub_type: CompositeSolidSubType::Union,
7467 output_index: None,
7468 solid_ids: vec![first_sweep_id, second_sweep_id],
7469 tool_ids: Vec::new(),
7470 code_ref,
7471 composite_solid_id: None,
7472 pattern_ids: Vec::new(),
7473 }),
7474 );
7475 let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7476 assert_eq!(
7477 solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7478 None
7479 );
7480 assert_eq!(
7481 solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7482 None
7483 );
7484 }
7485
7486 #[test]
7487 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7488 let source = "\
7489region001 = region(point = [0.1, 0.1], sketch = s)
7490extrude001 = extrude(region001, length = 5)
7491revolve001 = revolve(region001, axis = Y)
7492sweep001 = sweep(region001, path = path001)
7493loft001 = loft(region001)
7494not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7495";
7496
7497 let program = Program::parse(source).unwrap().0.unwrap();
7498
7499 assert_eq!(
7500 region_name_from_sweep_variable(&program.ast, "extrude001"),
7501 Some("region001".to_owned())
7502 );
7503 assert_eq!(
7504 region_name_from_sweep_variable(&program.ast, "revolve001"),
7505 Some("region001".to_owned())
7506 );
7507 assert_eq!(
7508 region_name_from_sweep_variable(&program.ast, "sweep001"),
7509 Some("region001".to_owned())
7510 );
7511 assert_eq!(
7512 region_name_from_sweep_variable(&program.ast, "loft001"),
7513 Some("region001".to_owned())
7514 );
7515 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7516 }
7517
7518 #[track_caller]
7519 fn expect_sketch(object: &Object) -> &Sketch {
7520 if let ObjectKind::Sketch(sketch) = &object.kind {
7521 sketch
7522 } else {
7523 panic!("Object is not a sketch: {:?}", object);
7524 }
7525 }
7526
7527 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7528 let point_object = scene_graph.objects.get(point_id.0).unwrap();
7529 let ObjectKind::Segment {
7530 segment: Segment::Point(point),
7531 } = &point_object.kind
7532 else {
7533 panic!("Object is not a point segment: {point_object:?}");
7534 };
7535 point.position.clone()
7536 }
7537
7538 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7539 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7540 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7541 }
7542
7543 fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7546 Point2d {
7547 x: Expr::Var(Number {
7548 value: x,
7549 units: NumericSuffix::Mm,
7550 }),
7551 y: Expr::Var(Number {
7552 value: y,
7553 units: NumericSuffix::Mm,
7554 }),
7555 }
7556 }
7557
7558 fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7561 Point2d {
7562 x: Number {
7563 value: x,
7564 units: NumericSuffix::Mm,
7565 },
7566 y: Number {
7567 value: y,
7568 units: NumericSuffix::Mm,
7569 },
7570 }
7571 }
7572
7573 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7574 LineCtor {
7575 start: Point2d {
7576 x: Expr::Number(Number { value: start_x, units }),
7577 y: Expr::Number(Number { value: start_y, units }),
7578 },
7579 end: Point2d {
7580 x: Expr::Number(Number { value: end_x, units }),
7581 y: Expr::Number(Number { value: end_y, units }),
7582 },
7583 construction: None,
7584 }
7585 }
7586
7587 async fn create_sketch_with_single_line(
7588 frontend: &mut FrontendState,
7589 ctx: &ExecutorContext,
7590 mock_ctx: &ExecutorContext,
7591 version: Version,
7592 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7593 frontend.program = Program::empty();
7594
7595 let sketch_args = SketchCtor {
7596 on: Plane::Default(PlaneName::Xy),
7597 };
7598 let (_src_delta, _scene_delta, sketch_id) = frontend
7599 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7600 .await
7601 .unwrap();
7602
7603 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7604 let (source_delta, scene_graph_delta) = frontend
7605 .add_segment(mock_ctx, version, sketch_id, segment, None)
7606 .await
7607 .unwrap();
7608 let line_id = *scene_graph_delta
7609 .new_objects
7610 .last()
7611 .expect("Expected line object id to be created");
7612
7613 (sketch_id, line_id, source_delta, scene_graph_delta)
7614 }
7615
7616 async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7617 frontend.program = program.clone();
7618 let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7619 frontend.update_state_after_exec(outcome, true);
7620 }
7621
7622 #[test]
7623 fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7624 for (source, expected_message) in [
7625 ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7626 ("3'", "Error parsing KCL source after editing: found unknown token '''"),
7627 ] {
7628 let err = parse_frontend_mutation_source(
7629 source,
7630 "Error parsing KCL source after editing",
7631 "No AST produced after editing",
7632 )
7633 .expect_err("expected invalid KCL source to fail");
7634 let message = err.error.message();
7635
7636 assert_eq!(message, expected_message);
7637 assert!(!message.contains("CompilationIssue"));
7638 assert!(!message.contains("KclErrorDetails"));
7639 assert!(!message.contains("source_range"));
7640 }
7641 }
7642
7643 #[tokio::test(flavor = "multi_thread")]
7644 async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7645 let initial_source = "\
7646sketch(on = XY) {
7647 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7648 distance([line1.start, line1.end]) == 10
7649}
7650";
7651 let program = Program::parse(initial_source).unwrap().0.unwrap();
7652
7653 let mut frontend = FrontendState::new();
7654 let mock_ctx = ExecutorContext::new_mock(None).await;
7655 let version = Version(0);
7656
7657 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7658 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7659 let sketch_id = sketch_object.id;
7660 let sketch = expect_sketch(sketch_object);
7661 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7662
7663 for (value, expected_message) in [
7664 ("**", "Invalid constraint value: Unexpected token: *"),
7665 ("3'", "Invalid constraint value: found unknown token '''"),
7666 ] {
7667 let err = frontend
7668 .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7669 .await
7670 .expect_err("expected invalid constraint expression to fail");
7671 let message = err.error.message();
7672
7673 assert_eq!(message, expected_message);
7674 assert!(!message.contains("CompilationIssue"));
7675 assert!(!message.contains("KclErrorDetails"));
7676 assert!(!message.contains("source_range"));
7677 }
7678
7679 mock_ctx.close().await;
7680 }
7681
7682 #[tokio::test(flavor = "multi_thread")]
7683 async fn test_failed_edit_constraint_value_does_not_update_program() {
7684 let initial_source = "\
7685sketch(on = XY) {
7686 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7687 distance([line1.start, line1.end]) == 10
7688}
7689";
7690 let program = Program::parse(initial_source).unwrap().0.unwrap();
7691 let original_source = program.original_file_contents.clone();
7692
7693 let mut frontend = FrontendState::new();
7694 let mock_ctx = ExecutorContext::new_mock(None).await;
7695 let version = Version(0);
7696
7697 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7698 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7699 let sketch_id = sketch_object.id;
7700 let sketch = expect_sketch(sketch_object);
7701 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7702
7703 frontend
7704 .edit_constraint_value(
7705 &mock_ctx,
7706 version,
7707 sketch_id,
7708 constraint_id,
7709 "unknownDistance".to_owned(),
7710 )
7711 .await
7712 .expect_err("expected invalid constraint value to fail execution");
7713
7714 assert_eq!(frontend.program.original_file_contents, original_source);
7715 assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7716
7717 mock_ctx.close().await;
7718 }
7719
7720 #[tokio::test(flavor = "multi_thread")]
7721 async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7722 let initial_source = "\
7723arr = [0]
7724sketch(on = XY) {
7725 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7726 distance([line1.start, line1.end]) == 10
7727}
7728";
7729 let program = Program::parse(initial_source).unwrap().0.unwrap();
7730
7731 let mut frontend = FrontendState::new();
7732 let mock_ctx = ExecutorContext::new_mock(None).await;
7733 let version = Version(0);
7734
7735 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7736 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7737 let sketch_id = sketch_object.id;
7738 let sketch = expect_sketch(sketch_object);
7739 let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7740
7741 let err = frontend
7745 .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7746 .await
7747 .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7748 let message = err.error.message();
7749 assert!(
7750 message.contains("The array doesn't have any item at index 5"),
7751 "unexpected error message: {message}"
7752 );
7753
7754 mock_ctx.close().await;
7755 }
7756
7757 #[tokio::test(flavor = "multi_thread")]
7758 async fn test_sketch_checkpoint_round_trip_restores_state() {
7759 let mut frontend = FrontendState::new();
7760 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7761 let mock_ctx = ExecutorContext::new_mock(None).await;
7762 let version = Version(0);
7763
7764 let (sketch_id, line_id, source_delta, scene_graph_delta) =
7765 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7766
7767 let expected_source = source_delta.text.clone();
7768 let expected_scene_graph = frontend.scene_graph.clone();
7769 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7770 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7771
7772 let checkpoint_id = frontend
7773 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7774 .await
7775 .unwrap();
7776
7777 let edited_segments = vec![ExistingSegmentCtor {
7778 id: line_id,
7779 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7780 }];
7781 let (edited_source, _edited_scene) = frontend
7782 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7783 .await
7784 .unwrap();
7785 assert_ne!(edited_source.text, expected_source);
7786
7787 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7788
7789 assert_eq!(restored.source_delta.text, expected_source);
7790 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7791 assert!(restored.scene_graph_delta.invalidates_ids);
7792 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7793 assert_eq!(frontend.scene_graph, expected_scene_graph);
7794 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7795
7796 ctx.close().await;
7797 }
7798
7799 #[tokio::test(flavor = "multi_thread")]
7800 async fn test_sketch_checkpoints_prune_oldest_entries() {
7801 let mut frontend = FrontendState::new();
7802 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7803 let mock_ctx = ExecutorContext::new_mock(None).await;
7804 let version = Version(0);
7805
7806 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7807 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7808
7809 let mut checkpoint_ids = Vec::new();
7810 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7811 checkpoint_ids.push(
7812 frontend
7813 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7814 .await
7815 .unwrap(),
7816 );
7817 }
7818
7819 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7820 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7821
7822 let oldest_retained = checkpoint_ids[3];
7823 assert_eq!(
7824 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7825 Some(oldest_retained)
7826 );
7827
7828 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7829 assert!(evicted_restore.is_err());
7830 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7831
7832 frontend
7833 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7834 .await
7835 .unwrap();
7836
7837 ctx.close().await;
7838 }
7839
7840 #[tokio::test(flavor = "multi_thread")]
7841 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7842 let mut frontend = FrontendState::new();
7843 let missing_checkpoint = SketchCheckpointId::new(999);
7844
7845 let err = frontend
7846 .restore_sketch_checkpoint(missing_checkpoint)
7847 .await
7848 .expect_err("Expected restore to fail for missing checkpoint");
7849
7850 assert!(err.msg.contains("Sketch checkpoint not found"));
7851 }
7852
7853 #[tokio::test(flavor = "multi_thread")]
7854 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7855 let mut frontend = FrontendState::new();
7856 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7857 let mock_ctx = ExecutorContext::new_mock(None).await;
7858 let version = Version(0);
7859
7860 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7861 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7862
7863 let checkpoint_a = frontend
7864 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7865 .await
7866 .unwrap();
7867 let checkpoint_b = frontend
7868 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7869 .await
7870 .unwrap();
7871 assert_eq!(frontend.sketch_checkpoints.len(), 2);
7872
7873 frontend.clear_sketch_checkpoints();
7874 assert!(frontend.sketch_checkpoints.is_empty());
7875 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7876 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7877
7878 ctx.close().await;
7879 }
7880
7881 #[tokio::test(flavor = "multi_thread")]
7882 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7883 let mut frontend = FrontendState::new();
7884 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7885 let mock_ctx = ExecutorContext::new_mock(None).await;
7886 let version = Version(0);
7887
7888 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7889 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7890 let old_source = source_delta.text.clone();
7891 let old_checkpoint = frontend
7892 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7893 .await
7894 .unwrap();
7895 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7896
7897 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
7898 .unwrap()
7899 .0
7900 .unwrap();
7901
7902 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7903 let SetProgramOutcome::Success {
7904 checkpoint_id: Some(new_checkpoint),
7905 ..
7906 } = result
7907 else {
7908 panic!("Expected Success with a fresh checkpoint baseline");
7909 };
7910
7911 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7912
7913 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7914 assert_eq!(old_restore.source_delta.text, old_source);
7915
7916 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7917 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7918
7919 ctx.close().await;
7920 }
7921
7922 #[tokio::test(flavor = "multi_thread")]
7923 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7924 let mut frontend = FrontendState::new();
7925 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7926 let mock_ctx = ExecutorContext::new_mock(None).await;
7927 let version = Version(0);
7928
7929 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7930 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7931 let old_checkpoint = frontend
7932 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7933 .await
7934 .unwrap();
7935 let checkpoint_count_before = frontend.sketch_checkpoints.len();
7936
7937 let failing_program = Program::parse(
7938 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7939 )
7940 .unwrap()
7941 .0
7942 .unwrap();
7943
7944 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7945 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7946 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7947 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7948
7949 ctx.close().await;
7950 }
7951
7952 #[tokio::test(flavor = "multi_thread")]
7953 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7954 let mut frontend = FrontendState::new();
7955 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7956
7957 let program = Program::parse(
7958 "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",
7959 )
7960 .unwrap()
7961 .0
7962 .unwrap();
7963 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7964 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7965 panic!("Expected successful baseline program execution");
7966 };
7967
7968 clear_mem_cache().await;
7969 assert!(read_old_memory().await.is_none());
7970
7971 let checkpoint_without_mock_memory = frontend
7972 .create_sketch_checkpoint((*exec_outcome).clone())
7973 .await
7974 .unwrap();
7975
7976 write_old_memory(SketchModeState::new_for_tests()).await;
7977 assert!(read_old_memory().await.is_some());
7978
7979 let checkpoint_with_mock_memory = frontend
7980 .create_sketch_checkpoint((*exec_outcome).clone())
7981 .await
7982 .unwrap();
7983
7984 clear_mem_cache().await;
7985 assert!(read_old_memory().await.is_none());
7986
7987 frontend
7988 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7989 .await
7990 .unwrap();
7991 assert!(read_old_memory().await.is_some());
7992
7993 frontend
7994 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7995 .await
7996 .unwrap();
7997 assert!(read_old_memory().await.is_none());
7998
7999 ctx.close().await;
8000 }
8001
8002 #[tokio::test(flavor = "multi_thread")]
8003 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
8004 let source = "\
8005sketch(on = XY) {
8006 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
8007}
8008
8009bad = missing_name
8010";
8011 let program = Program::parse(source).unwrap().0.unwrap();
8012
8013 let mut frontend = FrontendState::new();
8014
8015 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8016 let mock_ctx = ExecutorContext::new_mock(None).await;
8017 let version = Version(0);
8018 let project_id = ProjectId(0);
8019 let file_id = FileId(0);
8020
8021 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
8022 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
8023 };
8024
8025 let sketch_id = frontend
8026 .scene_graph
8027 .objects
8028 .iter()
8029 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
8030 .expect("Expected sketch object from errored hack_set_program");
8031
8032 frontend
8033 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
8034 .await
8035 .unwrap();
8036
8037 ctx.close().await;
8038 mock_ctx.close().await;
8039 }
8040
8041 #[tokio::test(flavor = "multi_thread")]
8042 async fn test_new_sketch_add_point_edit_point() {
8043 let program = Program::empty();
8044
8045 let mut frontend = FrontendState::new();
8046 frontend.program = program;
8047
8048 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8049 let mock_ctx = ExecutorContext::new_mock(None).await;
8050 let version = Version(0);
8051
8052 let sketch_args = SketchCtor {
8053 on: Plane::Default(PlaneName::Xy),
8054 };
8055 let (_src_delta, scene_delta, sketch_id) = frontend
8056 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8057 .await
8058 .unwrap();
8059 assert_eq!(sketch_id, ObjectId(1));
8060 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8061 let sketch_object = &scene_delta.new_graph.objects[1];
8062 assert_eq!(sketch_object.id, ObjectId(1));
8063 assert_eq!(
8064 sketch_object.kind,
8065 ObjectKind::Sketch(Sketch {
8066 args: SketchCtor {
8067 on: Plane::Default(PlaneName::Xy)
8068 },
8069 plane: ObjectId(0),
8070 segments: vec![],
8071 constraints: vec![],
8072 })
8073 );
8074 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8075
8076 let point_ctor = PointCtor {
8077 position: Point2d {
8078 x: Expr::Number(Number {
8079 value: 1.0,
8080 units: NumericSuffix::Inch,
8081 }),
8082 y: Expr::Number(Number {
8083 value: 2.0,
8084 units: NumericSuffix::Inch,
8085 }),
8086 },
8087 };
8088 let segment = SegmentCtor::Point(point_ctor);
8089 let (src_delta, scene_delta) = frontend
8090 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8091 .await
8092 .unwrap();
8093 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8094 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8095 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8096 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8097 assert_eq!(scene_object.id.0, i);
8098 }
8099
8100 let point_id = *scene_delta.new_objects.last().unwrap();
8101
8102 let point_ctor = PointCtor {
8103 position: Point2d {
8104 x: Expr::Number(Number {
8105 value: 3.0,
8106 units: NumericSuffix::Inch,
8107 }),
8108 y: Expr::Number(Number {
8109 value: 4.0,
8110 units: NumericSuffix::Inch,
8111 }),
8112 },
8113 };
8114 let segments = vec![ExistingSegmentCtor {
8115 id: point_id,
8116 ctor: SegmentCtor::Point(point_ctor),
8117 }];
8118 let (src_delta, scene_delta) = frontend
8119 .edit_segments(&mock_ctx, version, sketch_id, segments)
8120 .await
8121 .unwrap();
8122 insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8123 assert_eq!(scene_delta.new_objects, vec![]);
8124 assert_eq!(scene_delta.new_graph.objects.len(), 3);
8125
8126 ctx.close().await;
8127 mock_ctx.close().await;
8128 }
8129
8130 #[tokio::test(flavor = "multi_thread")]
8131 async fn test_new_sketch_add_line_edit_line() {
8132 let program = Program::empty();
8133
8134 let mut frontend = FrontendState::new();
8135 frontend.program = program;
8136
8137 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8138 let mock_ctx = ExecutorContext::new_mock(None).await;
8139 let version = Version(0);
8140
8141 let sketch_args = SketchCtor {
8142 on: Plane::Default(PlaneName::Xy),
8143 };
8144 let (_src_delta, scene_delta, sketch_id) = frontend
8145 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8146 .await
8147 .unwrap();
8148 assert_eq!(sketch_id, ObjectId(1));
8149 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8150 let sketch_object = &scene_delta.new_graph.objects[1];
8151 assert_eq!(sketch_object.id, ObjectId(1));
8152 assert_eq!(
8153 sketch_object.kind,
8154 ObjectKind::Sketch(Sketch {
8155 args: SketchCtor {
8156 on: Plane::Default(PlaneName::Xy)
8157 },
8158 plane: ObjectId(0),
8159 segments: vec![],
8160 constraints: vec![],
8161 })
8162 );
8163 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8164
8165 let line_ctor = LineCtor {
8166 start: Point2d {
8167 x: Expr::Number(Number {
8168 value: 0.0,
8169 units: NumericSuffix::Mm,
8170 }),
8171 y: Expr::Number(Number {
8172 value: 0.0,
8173 units: NumericSuffix::Mm,
8174 }),
8175 },
8176 end: Point2d {
8177 x: Expr::Number(Number {
8178 value: 10.0,
8179 units: NumericSuffix::Mm,
8180 }),
8181 y: Expr::Number(Number {
8182 value: 10.0,
8183 units: NumericSuffix::Mm,
8184 }),
8185 },
8186 construction: None,
8187 };
8188 let segment = SegmentCtor::Line(line_ctor);
8189 let (src_delta, scene_delta) = frontend
8190 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8191 .await
8192 .unwrap();
8193 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8194 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8195 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8196 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8197 assert_eq!(scene_object.id.0, i);
8198 }
8199
8200 let line = *scene_delta.new_objects.last().unwrap();
8202
8203 let line_ctor = LineCtor {
8204 start: Point2d {
8205 x: Expr::Number(Number {
8206 value: 1.0,
8207 units: NumericSuffix::Mm,
8208 }),
8209 y: Expr::Number(Number {
8210 value: 2.0,
8211 units: NumericSuffix::Mm,
8212 }),
8213 },
8214 end: Point2d {
8215 x: Expr::Number(Number {
8216 value: 13.0,
8217 units: NumericSuffix::Mm,
8218 }),
8219 y: Expr::Number(Number {
8220 value: 14.0,
8221 units: NumericSuffix::Mm,
8222 }),
8223 },
8224 construction: None,
8225 };
8226 let segments = vec![ExistingSegmentCtor {
8227 id: line,
8228 ctor: SegmentCtor::Line(line_ctor),
8229 }];
8230 let (src_delta, scene_delta) = frontend
8231 .edit_segments(&mock_ctx, version, sketch_id, segments)
8232 .await
8233 .unwrap();
8234 insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8235 assert_eq!(scene_delta.new_objects, vec![]);
8236 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8237
8238 ctx.close().await;
8239 mock_ctx.close().await;
8240 }
8241
8242 #[tokio::test(flavor = "multi_thread")]
8243 async fn test_new_sketch_add_arc_edit_arc() {
8244 let program = Program::empty();
8245
8246 let mut frontend = FrontendState::new();
8247 frontend.program = program;
8248
8249 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8250 let mock_ctx = ExecutorContext::new_mock(None).await;
8251 let version = Version(0);
8252
8253 let sketch_args = SketchCtor {
8254 on: Plane::Default(PlaneName::Xy),
8255 };
8256 let (_src_delta, scene_delta, sketch_id) = frontend
8257 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8258 .await
8259 .unwrap();
8260 assert_eq!(sketch_id, ObjectId(1));
8261 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8262 let sketch_object = &scene_delta.new_graph.objects[1];
8263 assert_eq!(sketch_object.id, ObjectId(1));
8264 assert_eq!(
8265 sketch_object.kind,
8266 ObjectKind::Sketch(Sketch {
8267 args: SketchCtor {
8268 on: Plane::Default(PlaneName::Xy),
8269 },
8270 plane: ObjectId(0),
8271 segments: vec![],
8272 constraints: vec![],
8273 })
8274 );
8275 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8276
8277 let arc_ctor = ArcCtor {
8278 start: Point2d {
8279 x: Expr::Var(Number {
8280 value: 0.0,
8281 units: NumericSuffix::Mm,
8282 }),
8283 y: Expr::Var(Number {
8284 value: 0.0,
8285 units: NumericSuffix::Mm,
8286 }),
8287 },
8288 end: Point2d {
8289 x: Expr::Var(Number {
8290 value: 10.0,
8291 units: NumericSuffix::Mm,
8292 }),
8293 y: Expr::Var(Number {
8294 value: 10.0,
8295 units: NumericSuffix::Mm,
8296 }),
8297 },
8298 center: Point2d {
8299 x: Expr::Var(Number {
8300 value: 10.0,
8301 units: NumericSuffix::Mm,
8302 }),
8303 y: Expr::Var(Number {
8304 value: 0.0,
8305 units: NumericSuffix::Mm,
8306 }),
8307 },
8308 direction: None,
8309 construction: None,
8310 };
8311 let segment = SegmentCtor::Arc(arc_ctor);
8312 let (src_delta, scene_delta) = frontend
8313 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8314 .await
8315 .unwrap();
8316 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8317 assert_eq!(
8318 scene_delta.new_objects,
8319 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8320 );
8321 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8322 assert_eq!(scene_object.id.0, i);
8323 }
8324 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8325
8326 let arc = *scene_delta.new_objects.last().unwrap();
8328
8329 let arc_ctor = ArcCtor {
8330 start: Point2d {
8331 x: Expr::Var(Number {
8332 value: 1.0,
8333 units: NumericSuffix::Mm,
8334 }),
8335 y: Expr::Var(Number {
8336 value: 2.0,
8337 units: NumericSuffix::Mm,
8338 }),
8339 },
8340 end: Point2d {
8341 x: Expr::Var(Number {
8342 value: 13.0,
8343 units: NumericSuffix::Mm,
8344 }),
8345 y: Expr::Var(Number {
8346 value: 14.0,
8347 units: NumericSuffix::Mm,
8348 }),
8349 },
8350 center: Point2d {
8351 x: Expr::Var(Number {
8352 value: 13.0,
8353 units: NumericSuffix::Mm,
8354 }),
8355 y: Expr::Var(Number {
8356 value: 2.0,
8357 units: NumericSuffix::Mm,
8358 }),
8359 },
8360 direction: None,
8361 construction: None,
8362 };
8363 let segments = vec![ExistingSegmentCtor {
8364 id: arc,
8365 ctor: SegmentCtor::Arc(arc_ctor),
8366 }];
8367 let (src_delta, scene_delta) = frontend
8368 .edit_segments(&mock_ctx, version, sketch_id, segments)
8369 .await
8370 .unwrap();
8371 insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8372 assert_eq!(scene_delta.new_objects, vec![]);
8373 assert_eq!(scene_delta.new_graph.objects.len(), 6);
8374
8375 ctx.close().await;
8376 mock_ctx.close().await;
8377 }
8378
8379 #[tokio::test(flavor = "multi_thread")]
8380 async fn test_new_sketch_add_circle_edit_circle() {
8381 let program = Program::empty();
8382
8383 let mut frontend = FrontendState::new();
8384 frontend.program = program;
8385
8386 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8387 let mock_ctx = ExecutorContext::new_mock(None).await;
8388 let version = Version(0);
8389
8390 let sketch_args = SketchCtor {
8391 on: Plane::Default(PlaneName::Xy),
8392 };
8393 let (_src_delta, _scene_delta, sketch_id) = frontend
8394 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8395 .await
8396 .unwrap();
8397
8398 let circle_ctor = CircleCtor {
8400 start: Point2d {
8401 x: Expr::Var(Number {
8402 value: 5.0,
8403 units: NumericSuffix::Mm,
8404 }),
8405 y: Expr::Var(Number {
8406 value: 0.0,
8407 units: NumericSuffix::Mm,
8408 }),
8409 },
8410 center: Point2d {
8411 x: Expr::Var(Number {
8412 value: 0.0,
8413 units: NumericSuffix::Mm,
8414 }),
8415 y: Expr::Var(Number {
8416 value: 0.0,
8417 units: NumericSuffix::Mm,
8418 }),
8419 },
8420 construction: None,
8421 };
8422 let segment = SegmentCtor::Circle(circle_ctor);
8423 let (src_delta, scene_delta) = frontend
8424 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8425 .await
8426 .unwrap();
8427 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8428 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8430 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8431
8432 let circle = *scene_delta.new_objects.last().unwrap();
8433
8434 let circle_ctor = CircleCtor {
8436 start: Point2d {
8437 x: Expr::Var(Number {
8438 value: 10.0,
8439 units: NumericSuffix::Mm,
8440 }),
8441 y: Expr::Var(Number {
8442 value: 0.0,
8443 units: NumericSuffix::Mm,
8444 }),
8445 },
8446 center: Point2d {
8447 x: Expr::Var(Number {
8448 value: 3.0,
8449 units: NumericSuffix::Mm,
8450 }),
8451 y: Expr::Var(Number {
8452 value: 4.0,
8453 units: NumericSuffix::Mm,
8454 }),
8455 },
8456 construction: None,
8457 };
8458 let segments = vec![ExistingSegmentCtor {
8459 id: circle,
8460 ctor: SegmentCtor::Circle(circle_ctor),
8461 }];
8462 let (src_delta, scene_delta) = frontend
8463 .edit_segments(&mock_ctx, version, sketch_id, segments)
8464 .await
8465 .unwrap();
8466 insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8467 assert_eq!(scene_delta.new_objects, vec![]);
8468 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8469
8470 ctx.close().await;
8471 mock_ctx.close().await;
8472 }
8473
8474 #[tokio::test(flavor = "multi_thread")]
8475 async fn test_delete_circle() {
8476 let initial_source = "sketch001 = sketch(on = XY) {
8477 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8478}
8479";
8480
8481 let program = Program::parse(initial_source).unwrap().0.unwrap();
8482 let mut frontend = FrontendState::new();
8483
8484 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8485 let mock_ctx = ExecutorContext::new_mock(None).await;
8486 let version = Version(0);
8487
8488 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8489 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8490 let sketch_id = sketch_object.id;
8491 let sketch = expect_sketch(sketch_object);
8492
8493 assert_eq!(sketch.segments.len(), 3);
8495 let circle_id = sketch.segments[2];
8496
8497 let (src_delta, scene_delta) = frontend
8499 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8500 .await
8501 .unwrap();
8502 insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8503 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8504 let new_sketch = expect_sketch(new_sketch_object);
8505 assert_eq!(new_sketch.segments.len(), 0);
8506
8507 ctx.close().await;
8508 mock_ctx.close().await;
8509 }
8510
8511 #[tokio::test(flavor = "multi_thread")]
8512 async fn test_edit_circle_via_point() {
8513 let initial_source = "sketch001 = sketch(on = XY) {
8514 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8515}
8516";
8517
8518 let program = Program::parse(initial_source).unwrap().0.unwrap();
8519 let mut frontend = FrontendState::new();
8520
8521 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8522 let mock_ctx = ExecutorContext::new_mock(None).await;
8523 let version = Version(0);
8524
8525 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8526 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8527 let sketch_id = sketch_object.id;
8528 let sketch = expect_sketch(sketch_object);
8529
8530 let circle_id = sketch
8532 .segments
8533 .iter()
8534 .copied()
8535 .find(|seg_id| {
8536 matches!(
8537 &frontend.scene_graph.objects[seg_id.0].kind,
8538 ObjectKind::Segment {
8539 segment: Segment::Circle(_)
8540 }
8541 )
8542 })
8543 .expect("Expected a circle segment in sketch");
8544 let circle_object = &frontend.scene_graph.objects[circle_id.0];
8545 let ObjectKind::Segment {
8546 segment: Segment::Circle(circle),
8547 } = &circle_object.kind
8548 else {
8549 panic!("Expected circle segment, got: {:?}", circle_object.kind);
8550 };
8551 let start_point_id = circle.start;
8552
8553 let segments = vec![ExistingSegmentCtor {
8555 id: start_point_id,
8556 ctor: SegmentCtor::Point(PointCtor {
8557 position: Point2d {
8558 x: Expr::Var(Number {
8559 value: 7.0,
8560 units: NumericSuffix::Mm,
8561 }),
8562 y: Expr::Var(Number {
8563 value: 1.0,
8564 units: NumericSuffix::Mm,
8565 }),
8566 },
8567 }),
8568 }];
8569 let (src_delta, _scene_delta) = frontend
8570 .edit_segments(&mock_ctx, version, sketch_id, segments)
8571 .await
8572 .unwrap();
8573 insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8574
8575 ctx.close().await;
8576 mock_ctx.close().await;
8577 }
8578
8579 #[tokio::test(flavor = "multi_thread")]
8580 async fn test_add_line_when_sketch_block_uses_variable() {
8581 let initial_source = "s = sketch(on = XY) {}
8582";
8583
8584 let program = Program::parse(initial_source).unwrap().0.unwrap();
8585
8586 let mut frontend = FrontendState::new();
8587
8588 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8589 let mock_ctx = ExecutorContext::new_mock(None).await;
8590 let version = Version(0);
8591
8592 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8593 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8594 let sketch_id = sketch_object.id;
8595
8596 let line_ctor = LineCtor {
8597 start: Point2d {
8598 x: Expr::Number(Number {
8599 value: 0.0,
8600 units: NumericSuffix::Mm,
8601 }),
8602 y: Expr::Number(Number {
8603 value: 0.0,
8604 units: NumericSuffix::Mm,
8605 }),
8606 },
8607 end: Point2d {
8608 x: Expr::Number(Number {
8609 value: 10.0,
8610 units: NumericSuffix::Mm,
8611 }),
8612 y: Expr::Number(Number {
8613 value: 10.0,
8614 units: NumericSuffix::Mm,
8615 }),
8616 },
8617 construction: None,
8618 };
8619 let segment = SegmentCtor::Line(line_ctor);
8620 let (src_delta, scene_delta) = frontend
8621 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8622 .await
8623 .unwrap();
8624 insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8625 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8626 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8627
8628 ctx.close().await;
8629 mock_ctx.close().await;
8630 }
8631
8632 #[tokio::test(flavor = "multi_thread")]
8633 async fn test_new_sketch_add_line_delete_sketch() {
8634 let program = Program::empty();
8635
8636 let mut frontend = FrontendState::new();
8637 frontend.program = program;
8638
8639 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8640 let mock_ctx = ExecutorContext::new_mock(None).await;
8641 let version = Version(0);
8642
8643 let sketch_args = SketchCtor {
8644 on: Plane::Default(PlaneName::Xy),
8645 };
8646 let (_src_delta, scene_delta, sketch_id) = frontend
8647 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8648 .await
8649 .unwrap();
8650 assert_eq!(sketch_id, ObjectId(1));
8651 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8652 let sketch_object = &scene_delta.new_graph.objects[1];
8653 assert_eq!(sketch_object.id, ObjectId(1));
8654 assert_eq!(
8655 sketch_object.kind,
8656 ObjectKind::Sketch(Sketch {
8657 args: SketchCtor {
8658 on: Plane::Default(PlaneName::Xy)
8659 },
8660 plane: ObjectId(0),
8661 segments: vec![],
8662 constraints: vec![],
8663 })
8664 );
8665 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8666
8667 let line_ctor = LineCtor {
8668 start: Point2d {
8669 x: Expr::Number(Number {
8670 value: 0.0,
8671 units: NumericSuffix::Mm,
8672 }),
8673 y: Expr::Number(Number {
8674 value: 0.0,
8675 units: NumericSuffix::Mm,
8676 }),
8677 },
8678 end: Point2d {
8679 x: Expr::Number(Number {
8680 value: 10.0,
8681 units: NumericSuffix::Mm,
8682 }),
8683 y: Expr::Number(Number {
8684 value: 10.0,
8685 units: NumericSuffix::Mm,
8686 }),
8687 },
8688 construction: None,
8689 };
8690 let segment = SegmentCtor::Line(line_ctor);
8691 let (src_delta, scene_delta) = frontend
8692 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8693 .await
8694 .unwrap();
8695 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8696 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8697
8698 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8699 insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8700 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8701
8702 ctx.close().await;
8703 mock_ctx.close().await;
8704 }
8705
8706 #[tokio::test(flavor = "multi_thread")]
8707 async fn test_delete_sketch_when_sketch_block_uses_variable() {
8708 let initial_source = "s = sketch(on = XY) {}
8709";
8710
8711 let program = Program::parse(initial_source).unwrap().0.unwrap();
8712
8713 let mut frontend = FrontendState::new();
8714
8715 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8716 let version = Version(0);
8717
8718 frontend.hack_set_program(&ctx, program).await.unwrap();
8719 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8720 let sketch_id = sketch_object.id;
8721
8722 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8723 insta::assert_snapshot!(
8724 "test_delete_sketch_when_sketch_block_uses_variable",
8725 src_delta.text.as_str()
8726 );
8727 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8728
8729 ctx.close().await;
8730 }
8731
8732 #[tokio::test(flavor = "multi_thread")]
8733 async fn test_delete_sketch_after_comment() {
8734 let initial_source = "sketch001 = sketch(on = XZ) {
8735}
8736";
8737
8738 let program = Program::parse(initial_source).unwrap().0.unwrap();
8739 let mut frontend = FrontendState::new();
8740
8741 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8742 let version = Version(0);
8743
8744 frontend.hack_set_program(&ctx, program).await.unwrap();
8745 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8746 let sketch_id = sketch_object.id;
8747 let original_source = sketch_object.source.clone();
8748
8749 let commented_source = "// test 1
8750sketch001 = sketch(on = XZ) {
8751}
8752";
8753 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8754 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8755
8756 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8757 assert_eq!(cached_sketch_object.source, original_source);
8758
8759 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8760 assert!(
8761 !src_delta.text.contains("sketch001"),
8762 "sketch was not deleted: {}",
8763 src_delta.text
8764 );
8765 insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8767 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8768
8769 ctx.close().await;
8770 }
8771
8772 #[tokio::test(flavor = "multi_thread")]
8773 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8774 let initial_source = "sketch001 = sketch(on = XZ) {
8775}
8776foo = 1
8777";
8778
8779 let program = Program::parse(initial_source).unwrap().0.unwrap();
8780 let mut frontend = FrontendState::new();
8781
8782 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8783 let version = Version(0);
8784
8785 frontend.hack_set_program(&ctx, program).await.unwrap();
8786 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8787 let sketch_id = sketch_object.id;
8788
8789 let commented_source = "// keep me
8790sketch001 = sketch(on = XZ) {
8791}
8792foo = 1
8793";
8794 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8795 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8796
8797 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8798 insta::assert_snapshot!(
8800 "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8801 src_delta.text.as_str()
8802 );
8803
8804 ctx.close().await;
8805 }
8806
8807 #[tokio::test(flavor = "multi_thread")]
8808 async fn test_delete_segment_preserves_pre_comment() {
8809 let initial_source = "\
8810sketch(on = XY) {
8811 point(at = [var 1, var 2])
8812 // describe the middle point
8813 point(at = [var 3, var 4])
8814 point(at = [var 5, var 6])
8815}
8816";
8817
8818 let program = Program::parse(initial_source).unwrap().0.unwrap();
8819 let mut frontend = FrontendState::new();
8820
8821 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8822 let mock_ctx = ExecutorContext::new_mock(None).await;
8823 let version = Version(0);
8824
8825 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8826 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8827 let sketch_id = sketch_object.id;
8828 let sketch = expect_sketch(sketch_object);
8829
8830 let middle_point_id = *sketch.segments.get(1).unwrap();
8831
8832 let (src_delta, _scene_delta) = frontend
8833 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8834 .await
8835 .unwrap();
8836 insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8839
8840 ctx.close().await;
8841 mock_ctx.close().await;
8842 }
8843
8844 #[tokio::test(flavor = "multi_thread")]
8845 async fn test_delete_last_segment_preserves_pre_comment() {
8846 let initial_source = "\
8847sketch(on = XY) {
8848 point(at = [var 1, var 2])
8849 // describe the trailing point
8850 point(at = [var 3, var 4])
8851}
8852";
8853
8854 let program = Program::parse(initial_source).unwrap().0.unwrap();
8855 let mut frontend = FrontendState::new();
8856
8857 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8858 let mock_ctx = ExecutorContext::new_mock(None).await;
8859 let version = Version(0);
8860
8861 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8862 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8863 let sketch_id = sketch_object.id;
8864 let sketch = expect_sketch(sketch_object);
8865
8866 let last_point_id = *sketch.segments.last().unwrap();
8867
8868 let (src_delta, _scene_delta) = frontend
8869 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8870 .await
8871 .unwrap();
8872 insta::assert_snapshot!(
8875 "test_delete_last_segment_preserves_pre_comment",
8876 src_delta.text.as_str()
8877 );
8878
8879 ctx.close().await;
8880 mock_ctx.close().await;
8881 }
8882
8883 #[tokio::test(flavor = "multi_thread")]
8884 async fn test_delete_segment_drops_inline_trailing_comment() {
8885 let initial_source = "\
8886sketch(on = XY) {
8887 point(at = [var 1, var 2])
8888 point(at = [var 3, var 4]) // same-line note that gets dropped
8889 point(at = [var 5, var 6])
8890}
8891";
8892
8893 let program = Program::parse(initial_source).unwrap().0.unwrap();
8894 let mut frontend = FrontendState::new();
8895
8896 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8897 let mock_ctx = ExecutorContext::new_mock(None).await;
8898 let version = Version(0);
8899
8900 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8901 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8902 let sketch_id = sketch_object.id;
8903 let sketch = expect_sketch(sketch_object);
8904
8905 let middle_point_id = *sketch.segments.get(1).unwrap();
8906
8907 let (src_delta, _scene_delta) = frontend
8908 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8909 .await
8910 .unwrap();
8911 assert!(
8913 !src_delta.text.contains("same-line note"),
8914 "inline comment should have been removed: {}",
8915 src_delta.text
8916 );
8917
8918 ctx.close().await;
8919 mock_ctx.close().await;
8920 }
8921
8922 #[tokio::test(flavor = "multi_thread")]
8923 async fn test_delete_segments_preserves_block_comments_across_positions() {
8924 let initial_source = "\
8932sketch(on = XY) {
8933 /* above first - moves to middle */
8934 point(at = [var 1, var 2]) /* same-line on first - dropped */
8935 /* above middle - stays */
8936 point(at = [var 3, var 4])
8937 /* above last - moves to trailing meta */
8938 point(at = [var 5, var 6])
8939}
8940";
8941
8942 let program = Program::parse(initial_source).unwrap().0.unwrap();
8943 let mut frontend = FrontendState::new();
8944
8945 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8946 let mock_ctx = ExecutorContext::new_mock(None).await;
8947 let version = Version(0);
8948
8949 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8950 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8951 let sketch_id = sketch_object.id;
8952 let sketch = expect_sketch(sketch_object);
8953
8954 let first_point_id = *sketch.segments.first().unwrap();
8955 let last_point_id = *sketch.segments.last().unwrap();
8956
8957 let (src_delta, _scene_delta) = frontend
8958 .delete_objects(
8959 &mock_ctx,
8960 version,
8961 sketch_id,
8962 Vec::new(),
8963 vec![first_point_id, last_point_id],
8964 )
8965 .await
8966 .unwrap();
8967 insta::assert_snapshot!(
8968 "test_delete_segments_preserves_block_comments_across_positions",
8969 src_delta.text.as_str()
8970 );
8971
8972 ctx.close().await;
8973 mock_ctx.close().await;
8974 }
8975
8976 #[tokio::test(flavor = "multi_thread")]
8977 async fn test_edit_line_when_editing_its_start_point() {
8978 let initial_source = "\
8979sketch(on = XY) {
8980 line(start = [var 1, var 2], end = [var 3, var 4])
8981}
8982";
8983
8984 let program = Program::parse(initial_source).unwrap().0.unwrap();
8985
8986 let mut frontend = FrontendState::new();
8987
8988 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8989 let mock_ctx = ExecutorContext::new_mock(None).await;
8990 let version = Version(0);
8991
8992 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8993 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8994 let sketch_id = sketch_object.id;
8995 let sketch = expect_sketch(sketch_object);
8996
8997 let point_id = *sketch.segments.first().unwrap();
8998
8999 let point_ctor = PointCtor {
9000 position: Point2d {
9001 x: Expr::Var(Number {
9002 value: 5.0,
9003 units: NumericSuffix::Inch,
9004 }),
9005 y: Expr::Var(Number {
9006 value: 6.0,
9007 units: NumericSuffix::Inch,
9008 }),
9009 },
9010 };
9011 let segments = vec![ExistingSegmentCtor {
9012 id: point_id,
9013 ctor: SegmentCtor::Point(point_ctor),
9014 }];
9015 let (src_delta, scene_delta) = frontend
9016 .edit_segments(&mock_ctx, version, sketch_id, segments)
9017 .await
9018 .unwrap();
9019 insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
9020 assert_eq!(scene_delta.new_objects, vec![]);
9021 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9022
9023 ctx.close().await;
9024 mock_ctx.close().await;
9025 }
9026
9027 #[tokio::test(flavor = "multi_thread")]
9028 async fn test_edit_line_when_editing_its_end_point() {
9029 let initial_source = "\
9030sketch(on = XY) {
9031 line(start = [var 1, var 2], end = [var 3, var 4])
9032}
9033";
9034
9035 let program = Program::parse(initial_source).unwrap().0.unwrap();
9036
9037 let mut frontend = FrontendState::new();
9038
9039 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9040 let mock_ctx = ExecutorContext::new_mock(None).await;
9041 let version = Version(0);
9042
9043 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9044 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9045 let sketch_id = sketch_object.id;
9046 let sketch = expect_sketch(sketch_object);
9047 let point_id = *sketch.segments.get(1).unwrap();
9048
9049 let point_ctor = PointCtor {
9050 position: Point2d {
9051 x: Expr::Var(Number {
9052 value: 5.0,
9053 units: NumericSuffix::Inch,
9054 }),
9055 y: Expr::Var(Number {
9056 value: 6.0,
9057 units: NumericSuffix::Inch,
9058 }),
9059 },
9060 };
9061 let segments = vec![ExistingSegmentCtor {
9062 id: point_id,
9063 ctor: SegmentCtor::Point(point_ctor),
9064 }];
9065 let (src_delta, scene_delta) = frontend
9066 .edit_segments(&mock_ctx, version, sketch_id, segments)
9067 .await
9068 .unwrap();
9069 insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9070 assert_eq!(scene_delta.new_objects, vec![]);
9071 assert_eq!(
9072 scene_delta.new_graph.objects.len(),
9073 5,
9074 "{:#?}",
9075 scene_delta.new_graph.objects
9076 );
9077
9078 ctx.close().await;
9079 mock_ctx.close().await;
9080 }
9081
9082 #[tokio::test(flavor = "multi_thread")]
9083 async fn test_edit_line_with_coincident_feedback() {
9084 let initial_source = "\
9085sketch(on = XY) {
9086 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9087 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9088 fixed([line1.start, [0, 0]])
9089 coincident([line1.end, line2.start])
9090 equalLength([line1, line2])
9091}
9092";
9093
9094 let program = Program::parse(initial_source).unwrap().0.unwrap();
9095
9096 let mut frontend = FrontendState::new();
9097
9098 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9099 let mock_ctx = ExecutorContext::new_mock(None).await;
9100 let version = Version(0);
9101
9102 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9103 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9104 let sketch_id = sketch_object.id;
9105 let sketch = expect_sketch(sketch_object);
9106 let line2_end_id = *sketch.segments.get(4).unwrap();
9107
9108 let segments = vec![ExistingSegmentCtor {
9109 id: line2_end_id,
9110 ctor: SegmentCtor::Point(PointCtor {
9111 position: Point2d {
9112 x: Expr::Var(Number {
9113 value: 9.0,
9114 units: NumericSuffix::None,
9115 }),
9116 y: Expr::Var(Number {
9117 value: 10.0,
9118 units: NumericSuffix::None,
9119 }),
9120 },
9121 }),
9122 }];
9123 let (src_delta, scene_delta) = frontend
9124 .edit_segments(&mock_ctx, version, sketch_id, segments)
9125 .await
9126 .unwrap();
9127 insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9128 assert_eq!(
9129 scene_delta.new_graph.objects.len(),
9130 11,
9131 "{:#?}",
9132 scene_delta.new_graph.objects
9133 );
9134
9135 ctx.close().await;
9136 mock_ctx.close().await;
9137 }
9138
9139 #[tokio::test(flavor = "multi_thread")]
9140 async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9141 let initial_source = "\
9142sketch(on = XY) {
9143 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9144 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9145 fixed([line1.start, [0, 0]])
9146 coincident([line1.end, line2.start])
9147 equalLength([line1, line2])
9148}
9149";
9150
9151 let program = Program::parse(initial_source).unwrap().0.unwrap();
9152 let mut frontend = FrontendState::new();
9153 let mock_ctx = ExecutorContext::new_mock(None).await;
9154 let version = Version(0);
9155
9156 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9157 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9158 let sketch_id = sketch_object.id;
9159 let sketch = expect_sketch(sketch_object);
9160 let line2_end_id = *sketch.segments.get(4).unwrap();
9161
9162 let segments = vec![ExistingSegmentCtor {
9163 id: line2_end_id,
9164 ctor: SegmentCtor::Point(PointCtor {
9165 position: Point2d {
9166 x: Expr::Var(Number {
9167 value: 9.0,
9168 units: NumericSuffix::None,
9169 }),
9170 y: Expr::Var(Number {
9171 value: 10.0,
9172 units: NumericSuffix::None,
9173 }),
9174 },
9175 }),
9176 }];
9177 let (edited_source, _) = frontend
9178 .edit_segments(&mock_ctx, version, sketch_id, segments)
9179 .await
9180 .unwrap();
9181
9182 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9183 assert_eq!(mock_source.text, edited_source.text);
9184
9185 mock_ctx.close().await;
9186 }
9187
9188 #[tokio::test(flavor = "multi_thread")]
9191 async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9192 let initial_source = "\
9193sketch(on = XY) {
9194 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9195 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9196 fixed([line1.start, [0, 0]])
9197 coincident([line1.end, line2.start])
9198 equalLength([line1, line2])
9199}
9200";
9201
9202 let program = Program::parse(initial_source).unwrap().0.unwrap();
9203 let mut frontend = FrontendState::new();
9204 let mock_ctx = ExecutorContext::new_mock(None).await;
9205 let version = Version(0);
9206
9207 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9208 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9209 let sketch_id = sketch_object.id;
9210 let sketch = expect_sketch(sketch_object);
9211 let line2_end_id = *sketch.segments.get(4).unwrap();
9212
9213 let segments = vec![ExistingSegmentCtor {
9214 id: line2_end_id,
9215 ctor: SegmentCtor::Point(PointCtor {
9216 position: Point2d {
9217 x: Expr::Var(Number {
9218 value: 9.0,
9219 units: NumericSuffix::None,
9220 }),
9221 y: Expr::Var(Number {
9222 value: 10.0,
9223 units: NumericSuffix::None,
9224 }),
9225 },
9226 }),
9227 }];
9228 let (preview_source, preview_delta) = frontend
9229 .edit_segments_with_options(
9230 &mock_ctx,
9231 version,
9232 sketch_id,
9233 segments,
9234 EditSegmentsOptions {
9235 anchor_segment_ids: Some(vec![line2_end_id]),
9236 drag_anchors: Vec::new(),
9237 constraint_label_edits: Vec::new(),
9238 commit_solved_initial_guesses: false,
9239 },
9240 )
9241 .await
9242 .unwrap();
9243
9244 assert!(
9245 !preview_delta.exec_outcome.var_solutions.is_empty(),
9246 "preview solve should still solve and return geometry feedback"
9247 );
9248 assert!(
9249 preview_source
9250 .text
9251 .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9252 );
9253 assert!(
9254 preview_source
9255 .text
9256 .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9257 );
9258
9259 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9260 assert_eq!(mock_source.text, preview_source.text);
9261
9262 mock_ctx.close().await;
9263 }
9264
9265 #[tokio::test(flavor = "multi_thread")]
9266 async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9267 let initial_source = "\
9268sketch(on = XY) {
9269 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9270}
9271";
9272
9273 let program = Program::parse(initial_source).unwrap().0.unwrap();
9274 let mut frontend = FrontendState::new();
9275 let mock_ctx = ExecutorContext::new_mock(None).await;
9276 let version = Version(0);
9277
9278 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9279 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9280 let sketch_id = sketch_object.id;
9281 let sketch = expect_sketch(sketch_object);
9282 let line_end_id = *sketch.segments.get(1).unwrap();
9283
9284 let constraint = Constraint::Fixed(Fixed {
9285 points: vec![FixedPoint {
9286 point: line_end_id,
9287 position: Point2d {
9288 x: Number {
9289 value: 20.0,
9290 units: NumericSuffix::Mm,
9291 },
9292 y: Number {
9293 value: 0.0,
9294 units: NumericSuffix::Mm,
9295 },
9296 },
9297 }],
9298 });
9299 let (constraint_source, _) = frontend
9300 .add_constraint(&mock_ctx, version, sketch_id, constraint)
9301 .await
9302 .unwrap();
9303
9304 assert!(
9305 constraint_source
9306 .text
9307 .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9308 "{}",
9309 constraint_source.text
9310 );
9311 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9312 assert_eq!(mock_source.text, constraint_source.text);
9313
9314 mock_ctx.close().await;
9315 }
9316
9317 #[test]
9318 fn test_no_solver_feedback_preserves_original_source() {
9319 let initial_source = "\
9320@settings(defaultLengthUnit = in, kclVersion = 2.0)
9321cylinder = startSketchOn(XY)
9322 |> circle(center= [0, 0], radius= 22)
9323 |> extrude(length = 14)
9324";
9325 let mut frontend = FrontendState::new();
9326 frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9327 let outcome = ExecOutcome {
9328 variables: Default::default(),
9329 operations: Default::default(),
9330 artifact_graph: Default::default(),
9331 scene_objects: Default::default(),
9332 source_range_to_object: Default::default(),
9333 var_solutions: Default::default(),
9334 refactor_metadata: Default::default(),
9335 issues: Default::default(),
9336 filenames: Default::default(),
9337 default_planes: Default::default(),
9338 };
9339
9340 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9341
9342 assert_eq!(source_delta.text, initial_source);
9343 }
9344
9345 #[tokio::test(flavor = "multi_thread")]
9348 async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9349 let initial_source = "\
9350sketch(on = XY) {
9351 point1 = point(at = [var 0mm, var 0mm])
9352 point2 = point(at = [var 0mm, var 0mm])
9353 coincident([point1, point2])
9354}
9355";
9356
9357 let program = Program::parse(initial_source).unwrap().0.unwrap();
9358 let mut frontend = FrontendState::new();
9359 let mock_ctx = ExecutorContext::new_mock(None).await;
9360 let version = Version(0);
9361
9362 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9363 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9364 let sketch_id = sketch_object.id;
9365 let sketch = expect_sketch(sketch_object);
9366 let point1_id = sketch.segments[0];
9367 let point2_id = sketch.segments[1];
9368
9369 let segments = vec![
9370 ExistingSegmentCtor {
9371 id: point1_id,
9372 ctor: SegmentCtor::Point(PointCtor {
9373 position: point_expr_mm(10.0, 0.0),
9374 }),
9375 },
9376 ExistingSegmentCtor {
9377 id: point2_id,
9378 ctor: SegmentCtor::Point(PointCtor {
9379 position: point_expr_mm(100.0, 0.0),
9380 }),
9381 },
9382 ];
9383 let (_, scene_delta) = frontend
9384 .edit_segments_with_options(
9385 &mock_ctx,
9386 version,
9387 sketch_id,
9388 segments,
9389 EditSegmentsOptions {
9390 anchor_segment_ids: Some(vec![point1_id]),
9391 drag_anchors: Vec::new(),
9392 constraint_label_edits: Vec::new(),
9393 commit_solved_initial_guesses: true,
9394 },
9395 )
9396 .await
9397 .unwrap();
9398
9399 assert_point_position_close(
9400 point_position(&scene_delta.new_graph, point1_id),
9401 point_number_mm(10.0, 0.0),
9402 );
9403 assert_point_position_close(
9404 point_position(&scene_delta.new_graph, point2_id),
9405 point_number_mm(10.0, 0.0),
9406 );
9407
9408 mock_ctx.close().await;
9409 }
9410
9411 fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9414 use std::cell::RefCell;
9415 struct Collector {
9416 target: f64,
9417 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9418 }
9419 impl<'a> crate::walk::Visitor<'a> for &Collector {
9420 type Error = crate::front::Error;
9421 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9422 if let crate::walk::Node::SketchVar(sketch_var) = node
9423 && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9424 && (initial.value - self.target).abs() < 1e-9
9425 {
9426 self.out
9427 .borrow_mut()
9428 .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9429 }
9430 for child in node.children().iter() {
9431 if !child.visit(*self)? {
9432 return Ok(false);
9433 }
9434 }
9435 Ok(true)
9436 }
9437 }
9438 let collector = Collector {
9439 target: value,
9440 out: Default::default(),
9441 };
9442 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9443 collector.out.into_inner()
9444 }
9445
9446 fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9449 use std::cell::RefCell;
9450 struct Collector {
9451 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9452 }
9453 impl<'a> crate::walk::Visitor<'a> for &Collector {
9454 type Error = crate::front::Error;
9455 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9456 if let crate::walk::Node::SketchVar(sketch_var) = node
9457 && let Some(node_path) = &sketch_var.node_path
9458 {
9459 self.out
9460 .borrow_mut()
9461 .push((SourceRange::from(sketch_var), node_path.clone()));
9462 }
9463 for child in node.children().iter() {
9464 if !child.visit(*self)? {
9465 return Ok(false);
9466 }
9467 }
9468 Ok(true)
9469 }
9470 }
9471 let collector = Collector {
9472 out: Default::default(),
9473 };
9474 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9475 collector.out.into_inner()
9476 }
9477
9478 fn empty_exec_outcome_with_var_solutions(
9479 var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9480 ) -> ExecOutcome {
9481 ExecOutcome {
9482 variables: Default::default(),
9483 operations: Default::default(),
9484 artifact_graph: Default::default(),
9485 scene_objects: Default::default(),
9486 source_range_to_object: Default::default(),
9487 var_solutions,
9488 refactor_metadata: Default::default(),
9489 issues: Default::default(),
9490 filenames: Default::default(),
9491 default_planes: Default::default(),
9492 }
9493 }
9494
9495 #[test]
9499 fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9500 let initial_source = "\
9501sketch(on = XY) {
9502 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9503}
9504";
9505 let program = Program::parse(initial_source).unwrap().0.unwrap();
9506 let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9507 assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9508 let (literal_range, node_path) = matches.into_iter().next().unwrap();
9509
9510 let mut frontend = FrontendState::new();
9511 frontend.program = program;
9512
9513 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9514 literal_range,
9515 Some(node_path),
9516 Number {
9517 value: 25.0,
9518 units: NumericSuffix::Mm,
9519 },
9520 )]);
9521
9522 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9523
9524 insta::assert_snapshot!(
9525 "test_commit_var_solution_by_node_path_updates_sketch_var",
9526 source_delta.text
9527 );
9528 }
9529
9530 #[test]
9537 fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9538 let compact_source = "\
9539sketch(on = XY) {
9540 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9541}
9542";
9543 let padded_source = "\
9544// added comment\n// added comment\n\nsketch(on = XY) {
9545 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9546}
9547";
9548 let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9549 let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9550
9551 let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9552 .into_iter()
9553 .next()
9554 .expect("expected `var 10mm` in compact source");
9555 let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9556 .into_iter()
9557 .next()
9558 .expect("expected `var 10mm` in padded source");
9559
9560 assert_ne!(
9561 compact_match.0, padded_match.0,
9562 "byte offsets must differ for this test to be meaningful"
9563 );
9564 assert_eq!(
9565 compact_match.1, padded_match.1,
9566 "node paths must agree across whitespace; that's the whole point of NodePath",
9567 );
9568
9569 let mut frontend = FrontendState::new();
9570 frontend.program = padded_program;
9571
9572 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9574 compact_match.0,
9575 Some(compact_match.1),
9576 Number {
9577 value: 30.0,
9578 units: NumericSuffix::Mm,
9579 },
9580 )]);
9581
9582 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9583
9584 insta::assert_snapshot!(
9585 "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9586 source_delta.text
9587 );
9588 }
9589
9590 #[test]
9594 fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9595 let initial_source = "\
9596sketch(on = XY) {
9597 line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9598}
9599";
9600 let program = Program::parse(initial_source).unwrap().0.unwrap();
9601
9602 let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9603 .into_iter()
9604 .next()
9605 .expect("expected `var 10mm`");
9606 let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9607 .into_iter()
9608 .next()
9609 .expect("expected `var 20mm`");
9610
9611 let mut frontend = FrontendState::new();
9612 frontend.program = program;
9613
9614 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9616 var_20.0,
9617 Some(var_10.1),
9618 Number {
9619 value: 33.0,
9620 units: NumericSuffix::Mm,
9621 },
9622 )]);
9623
9624 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9625
9626 insta::assert_snapshot!(
9627 "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9628 source_delta.text
9629 );
9630 }
9631
9632 #[test]
9639 fn test_commit_var_solution_writes_back_into_bare_var() {
9640 let initial_source = "\
9641@settings(experimentalFeatures = allow, kclVersion = 2.0)
9642sketch(on = XY) {
9643 line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9644}
9645";
9646 let program = Program::parse(initial_source).unwrap().0.unwrap();
9647
9648 let bare = collect_all_sketch_vars(&program)
9651 .into_iter()
9652 .find(|(range, _)| {
9653 range.end() - range.start() == 3
9655 })
9656 .expect("expected at least one bare `var`");
9657
9658 let mut frontend = FrontendState::new();
9659 frontend.program = program;
9660
9661 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9662 bare.0,
9663 Some(bare.1),
9664 Number {
9665 value: 7.0,
9666 units: NumericSuffix::Mm,
9667 },
9668 )]);
9669
9670 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9671
9672 insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9676 }
9677
9678 #[tokio::test(flavor = "multi_thread")]
9679 async fn test_delete_point_without_var() {
9680 let initial_source = "\
9681sketch(on = XY) {
9682 point(at = [var 1, var 2])
9683 point(at = [var 3, var 4])
9684 point(at = [var 5, var 6])
9685}
9686";
9687
9688 let program = Program::parse(initial_source).unwrap().0.unwrap();
9689
9690 let mut frontend = FrontendState::new();
9691
9692 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9693 let mock_ctx = ExecutorContext::new_mock(None).await;
9694 let version = Version(0);
9695
9696 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9697 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9698 let sketch_id = sketch_object.id;
9699 let sketch = expect_sketch(sketch_object);
9700
9701 let point_id = *sketch.segments.get(1).unwrap();
9702
9703 let (src_delta, scene_delta) = frontend
9704 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9705 .await
9706 .unwrap();
9707 insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9708 assert_eq!(scene_delta.new_objects, vec![]);
9709 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9710
9711 ctx.close().await;
9712 mock_ctx.close().await;
9713 }
9714
9715 #[tokio::test(flavor = "multi_thread")]
9716 async fn test_delete_point_with_var() {
9717 let initial_source = "\
9718sketch(on = XY) {
9719 point(at = [var 1, var 2])
9720 point1 = point(at = [var 3, var 4])
9721 point(at = [var 5, var 6])
9722}
9723";
9724
9725 let program = Program::parse(initial_source).unwrap().0.unwrap();
9726
9727 let mut frontend = FrontendState::new();
9728
9729 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9730 let mock_ctx = ExecutorContext::new_mock(None).await;
9731 let version = Version(0);
9732
9733 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9734 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9735 let sketch_id = sketch_object.id;
9736 let sketch = expect_sketch(sketch_object);
9737
9738 let point_id = *sketch.segments.get(1).unwrap();
9739
9740 let (src_delta, scene_delta) = frontend
9741 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9742 .await
9743 .unwrap();
9744 insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9745 assert_eq!(scene_delta.new_objects, vec![]);
9746 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9747
9748 ctx.close().await;
9749 mock_ctx.close().await;
9750 }
9751
9752 #[tokio::test(flavor = "multi_thread")]
9753 async fn test_delete_multiple_points() {
9754 let initial_source = "\
9755sketch(on = XY) {
9756 point(at = [var 1, var 2])
9757 point1 = point(at = [var 3, var 4])
9758 point(at = [var 5, var 6])
9759}
9760";
9761
9762 let program = Program::parse(initial_source).unwrap().0.unwrap();
9763
9764 let mut frontend = FrontendState::new();
9765
9766 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9767 let mock_ctx = ExecutorContext::new_mock(None).await;
9768 let version = Version(0);
9769
9770 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9771 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9772 let sketch_id = sketch_object.id;
9773
9774 let sketch = expect_sketch(sketch_object);
9775
9776 let point1_id = *sketch.segments.first().unwrap();
9777 let point2_id = *sketch.segments.get(1).unwrap();
9778
9779 let (src_delta, scene_delta) = frontend
9780 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9781 .await
9782 .unwrap();
9783 insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9784 assert_eq!(scene_delta.new_objects, vec![]);
9785 assert_eq!(scene_delta.new_graph.objects.len(), 3);
9786
9787 ctx.close().await;
9788 mock_ctx.close().await;
9789 }
9790
9791 #[tokio::test(flavor = "multi_thread")]
9792 async fn test_delete_coincident_constraint() {
9793 let initial_source = "\
9794sketch(on = XY) {
9795 point1 = point(at = [var 1, var 2])
9796 point2 = point(at = [var 3, var 4])
9797 coincident([point1, point2])
9798 point(at = [var 5, var 6])
9799}
9800";
9801
9802 let program = Program::parse(initial_source).unwrap().0.unwrap();
9803
9804 let mut frontend = FrontendState::new();
9805
9806 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9807 let mock_ctx = ExecutorContext::new_mock(None).await;
9808 let version = Version(0);
9809
9810 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9811 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9812 let sketch_id = sketch_object.id;
9813 let sketch = expect_sketch(sketch_object);
9814
9815 let coincident_id = *sketch.constraints.first().unwrap();
9816
9817 let (src_delta, scene_delta) = frontend
9818 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9819 .await
9820 .unwrap();
9821 insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9822 assert_eq!(scene_delta.new_objects, vec![]);
9823 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9824
9825 ctx.close().await;
9826 mock_ctx.close().await;
9827 }
9828
9829 #[tokio::test(flavor = "multi_thread")]
9830 async fn test_delete_line_cascades_to_coincident_constraint() {
9831 let initial_source = "\
9832sketch(on = XY) {
9833 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9834 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9835 coincident([line1.end, line2.start])
9836}
9837";
9838
9839 let program = Program::parse(initial_source).unwrap().0.unwrap();
9840
9841 let mut frontend = FrontendState::new();
9842
9843 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9844 let mock_ctx = ExecutorContext::new_mock(None).await;
9845 let version = Version(0);
9846
9847 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9848 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9849 let sketch_id = sketch_object.id;
9850 let sketch = expect_sketch(sketch_object);
9851 let line_id = *sketch.segments.get(5).unwrap();
9852
9853 let (src_delta, scene_delta) = frontend
9854 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9855 .await
9856 .unwrap();
9857 insta::assert_snapshot!(
9858 "test_delete_line_cascades_to_coincident_constraint",
9859 src_delta.text.as_str()
9860 );
9861 assert_eq!(
9862 scene_delta.new_graph.objects.len(),
9863 5,
9864 "{:#?}",
9865 scene_delta.new_graph.objects
9866 );
9867
9868 ctx.close().await;
9869 mock_ctx.close().await;
9870 }
9871
9872 #[tokio::test(flavor = "multi_thread")]
9873 async fn test_delete_line_cascades_to_distance_constraint() {
9874 let initial_source = "\
9875sketch(on = XY) {
9876 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9877 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9878 distance([line1.end, line2.start]) == 10mm
9879}
9880";
9881
9882 let program = Program::parse(initial_source).unwrap().0.unwrap();
9883
9884 let mut frontend = FrontendState::new();
9885
9886 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9887 let mock_ctx = ExecutorContext::new_mock(None).await;
9888 let version = Version(0);
9889
9890 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9891 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9892 let sketch_id = sketch_object.id;
9893 let sketch = expect_sketch(sketch_object);
9894 let line_id = *sketch.segments.get(5).unwrap();
9895
9896 let (src_delta, scene_delta) = frontend
9897 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9898 .await
9899 .unwrap();
9900 insta::assert_snapshot!(
9901 "test_delete_line_cascades_to_distance_constraint",
9902 src_delta.text.as_str()
9903 );
9904 assert_eq!(
9905 scene_delta.new_graph.objects.len(),
9906 5,
9907 "{:#?}",
9908 scene_delta.new_graph.objects
9909 );
9910
9911 ctx.close().await;
9912 mock_ctx.close().await;
9913 }
9914
9915 #[tokio::test(flavor = "multi_thread")]
9916 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9917 let initial_source = "\
9918sketch(on = XY) {
9919 point1 = point(at = [var 1, var 2])
9920 point2 = point(at = [var 3, var 4])
9921 horizontalDistance([point1, point2]) == 10mm
9922}
9923";
9924
9925 let program = Program::parse(initial_source).unwrap().0.unwrap();
9926
9927 let mut frontend = FrontendState::new();
9928
9929 let mock_ctx = ExecutorContext::new_mock(None).await;
9930 let version = Version(0);
9931
9932 frontend.program = program.clone();
9933 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9934 frontend.update_state_after_exec(outcome, true);
9935 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9936 let sketch_id = sketch_object.id;
9937 let sketch = expect_sketch(sketch_object);
9938 let point2_id = *sketch.segments.get(1).unwrap();
9939
9940 let (src_delta, scene_delta) = frontend
9941 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9942 .await
9943 .unwrap();
9944 insta::assert_snapshot!(
9945 "test_delete_point_cascades_to_horizontal_distance_constraint",
9946 src_delta.text.as_str()
9947 );
9948 assert_eq!(
9949 scene_delta.new_graph.objects.len(),
9950 3,
9951 "{:#?}",
9952 scene_delta.new_graph.objects
9953 );
9954
9955 mock_ctx.close().await;
9956 }
9957
9958 #[tokio::test(flavor = "multi_thread")]
9959 async fn test_delete_line_cascades_to_fixed_constraint() {
9960 let initial_source = "\
9961sketch(on = XY) {
9962 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9963 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9964 fixed([line1.start, [0, 0]])
9965}
9966";
9967
9968 let program = Program::parse(initial_source).unwrap().0.unwrap();
9969
9970 let mut frontend = FrontendState::new();
9971
9972 let mock_ctx = ExecutorContext::new_mock(None).await;
9973 let version = Version(0);
9974
9975 frontend.program = program.clone();
9976 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9977 frontend.update_state_after_exec(outcome, true);
9978 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9979 let sketch_id = sketch_object.id;
9980 let sketch = expect_sketch(sketch_object);
9981 let line1_id = *sketch.segments.get(2).unwrap();
9982
9983 let (src_delta, scene_delta) = frontend
9984 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9985 .await
9986 .unwrap();
9987 insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9988 assert_eq!(
9989 scene_delta.new_graph.objects.len(),
9990 5,
9991 "{:#?}",
9992 scene_delta.new_graph.objects
9993 );
9994
9995 mock_ctx.close().await;
9996 }
9997
9998 #[tokio::test(flavor = "multi_thread")]
9999 async fn test_delete_line_cascades_to_midpoint_constraint() {
10000 let initial_source = "\
10001sketch(on = XY) {
10002 point1 = point(at = [var 1, var 2])
10003 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
10004 midpoint(line1, point = point1)
10005}
10006";
10007
10008 let program = Program::parse(initial_source).unwrap().0.unwrap();
10009
10010 let mut frontend = FrontendState::new();
10011
10012 let mock_ctx = ExecutorContext::new_mock(None).await;
10013 let version = Version(0);
10014
10015 frontend.program = program.clone();
10016 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10017 frontend.update_state_after_exec(outcome, true);
10018 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10019 let sketch_id = sketch_object.id;
10020 let sketch = expect_sketch(sketch_object);
10021 let line1_id = *sketch.segments.get(3).unwrap();
10022
10023 let (src_delta, scene_delta) = frontend
10024 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10025 .await
10026 .unwrap();
10027 insta::assert_snapshot!(
10028 "test_delete_line_cascades_to_midpoint_constraint",
10029 src_delta.text.as_str()
10030 );
10031 assert_eq!(
10032 scene_delta.new_graph.objects.len(),
10033 3,
10034 "{:#?}",
10035 scene_delta.new_graph.objects
10036 );
10037
10038 mock_ctx.close().await;
10039 }
10040
10041 #[tokio::test(flavor = "multi_thread")]
10042 async fn test_delete_point_preserves_multiline_coincident_constraint() {
10043 let initial_source = "\
10044sketch(on = XY) {
10045 point1 = point(at = [var 1, var 2])
10046 point2 = point(at = [var 3, var 4])
10047 point3 = point(at = [var 5, var 6])
10048 coincident([point1, point2, point3])
10049}
10050";
10051
10052 let program = Program::parse(initial_source).unwrap().0.unwrap();
10053
10054 let mut frontend = FrontendState::new();
10055
10056 let mock_ctx = ExecutorContext::new_mock(None).await;
10057 let version = Version(0);
10058
10059 frontend.program = program.clone();
10060 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10061 frontend.update_state_after_exec(outcome, true);
10062 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10063 let sketch_id = sketch_object.id;
10064 let sketch = expect_sketch(sketch_object);
10065 let point3_id = *sketch.segments.get(2).unwrap();
10066
10067 let (src_delta, scene_delta) = frontend
10068 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10069 .await
10070 .unwrap();
10071 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10072 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10073 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10074 assert!(
10075 src_delta.text.contains("coincident([point1, point2])"),
10076 "{}",
10077 src_delta.text
10078 );
10079
10080 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10081 let sketch = expect_sketch(sketch_object);
10082 assert_eq!(sketch.segments.len(), 2);
10083 assert_eq!(sketch.constraints.len(), 1);
10084
10085 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10086 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10087 panic!("Expected constraint object");
10088 };
10089 let Constraint::Coincident(coincident) = constraint else {
10090 panic!("Expected coincident constraint");
10091 };
10092 assert_eq!(
10093 coincident.segments,
10094 sketch
10095 .segments
10096 .iter()
10097 .copied()
10098 .map(Into::into)
10099 .collect::<Vec<ConstraintSegment>>()
10100 );
10101
10102 mock_ctx.close().await;
10103 }
10104
10105 #[tokio::test(flavor = "multi_thread")]
10106 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10107 let initial_source = "\
10108sketch(on = XY) {
10109 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10110 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10111 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10112 equalLength([line1, line2, line3])
10113}
10114";
10115
10116 let program = Program::parse(initial_source).unwrap().0.unwrap();
10117
10118 let mut frontend = FrontendState::new();
10119
10120 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10121 let mock_ctx = ExecutorContext::new_mock(None).await;
10122 let version = Version(0);
10123
10124 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10125 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10126 let sketch_id = sketch_object.id;
10127 let sketch = expect_sketch(sketch_object);
10128 let line3_id = *sketch.segments.get(8).unwrap();
10129
10130 let (src_delta, scene_delta) = frontend
10131 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10132 .await
10133 .unwrap();
10134 insta::assert_snapshot!(
10135 "test_delete_line_preserves_multiline_equal_length_constraint",
10136 src_delta.text.as_str()
10137 );
10138
10139 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10140 let sketch = expect_sketch(sketch_object);
10141 assert_eq!(sketch.constraints.len(), 1);
10142
10143 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10144 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10145 panic!("Expected constraint object");
10146 };
10147 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10148 panic!("Expected lines equal length constraint");
10149 };
10150 assert_eq!(lines_equal_length.lines.len(), 2);
10151
10152 ctx.close().await;
10153 mock_ctx.close().await;
10154 }
10155
10156 #[tokio::test(flavor = "multi_thread")]
10157 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10158 let initial_source = "\
10159sketch(on = XY) {
10160 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10161 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10162 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10163 horizontal([line1.end, line2.start, line3.start])
10164}
10165";
10166
10167 let program = Program::parse(initial_source).unwrap().0.unwrap();
10168
10169 let mut frontend = FrontendState::new();
10170
10171 let mock_ctx = ExecutorContext::new_mock(None).await;
10172 let version = Version(0);
10173
10174 frontend.program = program.clone();
10175 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10176 frontend.update_state_after_exec(outcome, true);
10177 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10178 let sketch_id = sketch_object.id;
10179 let sketch = expect_sketch(sketch_object);
10180 let line1_id = *sketch.segments.get(2).unwrap();
10181
10182 let (src_delta, scene_delta) = frontend
10183 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10184 .await
10185 .unwrap();
10186 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10187 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10188 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10189 assert!(
10190 src_delta.text.contains("horizontal([line2.start, line3.start])"),
10191 "{}",
10192 src_delta.text
10193 );
10194
10195 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10196 let sketch = expect_sketch(sketch_object);
10197 assert_eq!(sketch.constraints.len(), 1);
10198
10199 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10200 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10201 panic!("Expected constraint object");
10202 };
10203 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10204 panic!("Expected horizontal points constraint");
10205 };
10206 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10207 assert_eq!(*points, remaining_points);
10208
10209 mock_ctx.close().await;
10210 }
10211
10212 #[tokio::test(flavor = "multi_thread")]
10213 async fn test_delete_line_preserves_multiline_vertical_constraint() {
10214 let initial_source = "\
10215sketch(on = XY) {
10216 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10217 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10218 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10219 vertical([line1.end, line2.start, line3.start])
10220}
10221";
10222
10223 let program = Program::parse(initial_source).unwrap().0.unwrap();
10224
10225 let mut frontend = FrontendState::new();
10226
10227 let mock_ctx = ExecutorContext::new_mock(None).await;
10228 let version = Version(0);
10229
10230 frontend.program = program.clone();
10231 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10232 frontend.update_state_after_exec(outcome, true);
10233 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10234 let sketch_id = sketch_object.id;
10235 let sketch = expect_sketch(sketch_object);
10236 let line1_id = *sketch.segments.get(2).unwrap();
10237
10238 let (src_delta, scene_delta) = frontend
10239 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10240 .await
10241 .unwrap();
10242 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10243 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10244 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10245 assert!(
10246 src_delta.text.contains("vertical([line2.start, line3.start])"),
10247 "{}",
10248 src_delta.text
10249 );
10250
10251 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10252 let sketch = expect_sketch(sketch_object);
10253 assert_eq!(sketch.constraints.len(), 1);
10254
10255 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10256 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10257 panic!("Expected constraint object");
10258 };
10259 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10260 panic!("Expected vertical points constraint");
10261 };
10262 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10263 assert_eq!(*points, remaining_points);
10264
10265 mock_ctx.close().await;
10266 }
10267
10268 #[tokio::test(flavor = "multi_thread")]
10269 async fn test_delete_line_preserves_multiline_coincident_constraint() {
10270 let initial_source = "\
10271sketch(on = XY) {
10272 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10273 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10274 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10275 coincident([line1.end, line2.start, line3.start])
10276}
10277";
10278
10279 let program = Program::parse(initial_source).unwrap().0.unwrap();
10280
10281 let mut frontend = FrontendState::new();
10282
10283 let mock_ctx = ExecutorContext::new_mock(None).await;
10284 let version = Version(0);
10285
10286 frontend.program = program.clone();
10287 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10288 frontend.update_state_after_exec(outcome, true);
10289 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10290 let sketch_id = sketch_object.id;
10291 let sketch = expect_sketch(sketch_object);
10292 let line1_id = *sketch.segments.get(2).unwrap();
10293
10294 let (src_delta, scene_delta) = frontend
10295 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10296 .await
10297 .unwrap();
10298 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10299 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10300 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10301 assert!(
10302 src_delta.text.contains("coincident([line2.start, line3.start])"),
10303 "{}",
10304 src_delta.text
10305 );
10306
10307 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10308 let sketch = expect_sketch(sketch_object);
10309 assert_eq!(sketch.constraints.len(), 1);
10310
10311 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10312 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10313 panic!("Expected constraint object");
10314 };
10315 let Constraint::Coincident(coincident) = constraint else {
10316 panic!("Expected coincident constraint");
10317 };
10318 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10319 assert_eq!(coincident.segments, remaining_segments);
10320
10321 mock_ctx.close().await;
10322 }
10323
10324 #[tokio::test(flavor = "multi_thread")]
10325 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10326 let initial_source = "\
10327sketch(on = XY) {
10328 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10329 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10330 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10331 equalLength([line1, line2, line3])
10332}
10333";
10334
10335 let program = Program::parse(initial_source).unwrap().0.unwrap();
10336
10337 let mut frontend = FrontendState::new();
10338
10339 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10340 let mock_ctx = ExecutorContext::new_mock(None).await;
10341 let version = Version(0);
10342
10343 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10344 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10345 let sketch_id = sketch_object.id;
10346 let sketch = expect_sketch(sketch_object);
10347 let line2_id = *sketch.segments.get(5).unwrap();
10348 let line3_id = *sketch.segments.get(8).unwrap();
10349
10350 let (src_delta, scene_delta) = frontend
10351 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10352 .await
10353 .unwrap();
10354 insta::assert_snapshot!(
10355 "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10356 src_delta.text.as_str()
10357 );
10358
10359 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10360 let sketch = expect_sketch(sketch_object);
10361 assert!(sketch.constraints.is_empty());
10362
10363 ctx.close().await;
10364 mock_ctx.close().await;
10365 }
10366
10367 #[tokio::test(flavor = "multi_thread")]
10368 async fn test_delete_line_preserves_multiline_parallel_constraint() {
10369 let initial_source = "\
10370sketch(on = XY) {
10371 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10372 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10373 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10374 parallel([line1, line2, line3])
10375}
10376";
10377
10378 let program = Program::parse(initial_source).unwrap().0.unwrap();
10379
10380 let mut frontend = FrontendState::new();
10381
10382 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10383 let mock_ctx = ExecutorContext::new_mock(None).await;
10384 let version = Version(0);
10385
10386 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10387 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10388 let sketch_id = sketch_object.id;
10389 let sketch = expect_sketch(sketch_object);
10390 let line3_id = *sketch.segments.get(8).unwrap();
10391
10392 let (src_delta, scene_delta) = frontend
10393 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10394 .await
10395 .unwrap();
10396 insta::assert_snapshot!(
10397 "test_delete_line_preserves_multiline_parallel_constraint",
10398 src_delta.text.as_str()
10399 );
10400
10401 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10402 let sketch = expect_sketch(sketch_object);
10403 assert_eq!(sketch.constraints.len(), 1);
10404
10405 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10406 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10407 panic!("Expected constraint object");
10408 };
10409 let Constraint::Parallel(parallel) = constraint else {
10410 panic!("Expected parallel constraint");
10411 };
10412 assert_eq!(parallel.lines.len(), 2);
10413
10414 ctx.close().await;
10415 mock_ctx.close().await;
10416 }
10417
10418 #[tokio::test(flavor = "multi_thread")]
10419 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10420 let initial_source = "\
10421sketch(on = XY) {
10422 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10423 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10424 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10425 parallel([line1, line2, line3])
10426}
10427";
10428
10429 let program = Program::parse(initial_source).unwrap().0.unwrap();
10430
10431 let mut frontend = FrontendState::new();
10432
10433 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10434 let mock_ctx = ExecutorContext::new_mock(None).await;
10435 let version = Version(0);
10436
10437 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10438 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10439 let sketch_id = sketch_object.id;
10440 let sketch = expect_sketch(sketch_object);
10441 let line2_id = *sketch.segments.get(5).unwrap();
10442 let line3_id = *sketch.segments.get(8).unwrap();
10443
10444 let (src_delta, scene_delta) = frontend
10445 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10446 .await
10447 .unwrap();
10448 insta::assert_snapshot!(
10449 "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10450 src_delta.text.as_str()
10451 );
10452
10453 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10454 let sketch = expect_sketch(sketch_object);
10455 assert!(sketch.constraints.is_empty());
10456
10457 ctx.close().await;
10458 mock_ctx.close().await;
10459 }
10460
10461 #[tokio::test(flavor = "multi_thread")]
10462 async fn test_delete_line_line_coincident_constraint() {
10463 let initial_source = "\
10464sketch(on = XY) {
10465 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10466 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10467 coincident([line1, line2])
10468}
10469";
10470
10471 let program = Program::parse(initial_source).unwrap().0.unwrap();
10472
10473 let mut frontend = FrontendState::new();
10474
10475 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10476 let mock_ctx = ExecutorContext::new_mock(None).await;
10477 let version = Version(0);
10478
10479 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10480 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10481 let sketch_id = sketch_object.id;
10482 let sketch = expect_sketch(sketch_object);
10483
10484 let coincident_id = *sketch.constraints.first().unwrap();
10485
10486 let (src_delta, scene_delta) = frontend
10487 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10488 .await
10489 .unwrap();
10490 insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10491 assert_eq!(scene_delta.new_objects, vec![]);
10492 assert_eq!(scene_delta.new_graph.objects.len(), 8);
10493
10494 ctx.close().await;
10495 mock_ctx.close().await;
10496 }
10497
10498 #[tokio::test(flavor = "multi_thread")]
10499 async fn test_two_points_coincident() {
10500 let initial_source = "\
10501sketch(on = XY) {
10502 point1 = point(at = [var 1, var 2])
10503 point(at = [3, 4])
10504}
10505";
10506
10507 let program = Program::parse(initial_source).unwrap().0.unwrap();
10508
10509 let mut frontend = FrontendState::new();
10510
10511 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10512 let mock_ctx = ExecutorContext::new_mock(None).await;
10513 let version = Version(0);
10514
10515 frontend.hack_set_program(&ctx, program).await.unwrap();
10516 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10517 let sketch_id = sketch_object.id;
10518 let sketch = expect_sketch(sketch_object);
10519 let point0_id = *sketch.segments.first().unwrap();
10520 let point1_id = *sketch.segments.get(1).unwrap();
10521
10522 let constraint = Constraint::Coincident(Coincident {
10523 segments: vec![point0_id.into(), point1_id.into()],
10524 });
10525 let (src_delta, scene_delta) = frontend
10526 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10527 .await
10528 .unwrap();
10529 insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10530 assert_eq!(
10531 scene_delta.new_graph.objects.len(),
10532 5,
10533 "{:#?}",
10534 scene_delta.new_graph.objects
10535 );
10536
10537 ctx.close().await;
10538 mock_ctx.close().await;
10539 }
10540
10541 #[tokio::test(flavor = "multi_thread")]
10542 async fn test_three_points_coincident() {
10543 let initial_source = "\
10544sketch(on = XY) {
10545 point1 = point(at = [var 1, var 2])
10546 point(at = [var 3, var 4])
10547 point(at = [var 5, var 6])
10548}
10549";
10550
10551 let program = Program::parse(initial_source).unwrap().0.unwrap();
10552
10553 let mut frontend = FrontendState::new();
10554
10555 let mock_ctx = ExecutorContext::new_mock(None).await;
10556 let version = Version(0);
10557
10558 frontend.program = program.clone();
10559 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10560 frontend.update_state_after_exec(outcome, true);
10561 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10562 let sketch_id = sketch_object.id;
10563 let sketch = expect_sketch(sketch_object);
10564 let segments = sketch
10565 .segments
10566 .iter()
10567 .take(3)
10568 .copied()
10569 .map(Into::into)
10570 .collect::<Vec<ConstraintSegment>>();
10571
10572 let constraint = Constraint::Coincident(Coincident {
10573 segments: segments.clone(),
10574 });
10575 let (src_delta, scene_delta) = frontend
10576 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10577 .await
10578 .unwrap();
10579 insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10580
10581 let constraint_object = scene_delta
10582 .new_graph
10583 .objects
10584 .iter()
10585 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10586 .unwrap();
10587
10588 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10589 panic!("expected a constraint object");
10590 };
10591
10592 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10593
10594 mock_ctx.close().await;
10595 }
10596
10597 #[tokio::test(flavor = "multi_thread")]
10598 async fn test_source_with_three_point_coincident_tracks_all_segments() {
10599 let initial_source = "\
10600sketch(on = XY) {
10601 point1 = point(at = [var 1, var 2])
10602 point2 = point(at = [var 3, var 4])
10603 point3 = point(at = [var 5, var 6])
10604 coincident([point1, point2, point3])
10605}
10606";
10607
10608 let program = Program::parse(initial_source).unwrap().0.unwrap();
10609
10610 let mut frontend = FrontendState::new();
10611
10612 let ctx = ExecutorContext::new_mock(None).await;
10613 frontend.program = program.clone();
10614 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10615 frontend.update_state_after_exec(outcome, true);
10616
10617 let constraint_object = frontend
10618 .scene_graph
10619 .objects
10620 .iter()
10621 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10622 .unwrap();
10623 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10624 panic!("expected a constraint object");
10625 };
10626
10627 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10628 let sketch = expect_sketch(sketch_object);
10629 let expected_segments = sketch
10630 .segments
10631 .iter()
10632 .take(3)
10633 .copied()
10634 .map(Into::into)
10635 .collect::<Vec<ConstraintSegment>>();
10636
10637 assert_eq!(
10638 constraint,
10639 &Constraint::Coincident(Coincident {
10640 segments: expected_segments,
10641 })
10642 );
10643
10644 ctx.close().await;
10645 }
10646
10647 #[tokio::test(flavor = "multi_thread")]
10648 async fn test_point_origin_coincident_preserves_order() {
10649 let initial_source = "\
10650sketch(on = XY) {
10651 point(at = [var 1, var 2])
10652}
10653";
10654
10655 for (origin_first, snapshot_name) in [
10656 (true, "test_point_origin_coincident_preserves_order_origin_first"),
10657 (false, "test_point_origin_coincident_preserves_order_point_first"),
10658 ] {
10659 let program = Program::parse(initial_source).unwrap().0.unwrap();
10660
10661 let mut frontend = FrontendState::new();
10662
10663 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10664 let mock_ctx = ExecutorContext::new_mock(None).await;
10665 let version = Version(0);
10666
10667 frontend.hack_set_program(&ctx, program).await.unwrap();
10668 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10669 let sketch_id = sketch_object.id;
10670 let sketch = expect_sketch(sketch_object);
10671 let point_id = *sketch.segments.first().unwrap();
10672
10673 let segments = if origin_first {
10674 vec![ConstraintSegment::ORIGIN, point_id.into()]
10675 } else {
10676 vec![point_id.into(), ConstraintSegment::ORIGIN]
10677 };
10678 let constraint = Constraint::Coincident(Coincident {
10679 segments: segments.clone(),
10680 });
10681 let (src_delta, scene_delta) = frontend
10682 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10683 .await
10684 .unwrap();
10685 insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10686
10687 let constraint_object = scene_delta
10688 .new_graph
10689 .objects
10690 .iter()
10691 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10692 .unwrap();
10693
10694 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10695 panic!("expected a constraint object");
10696 };
10697
10698 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10699
10700 ctx.close().await;
10701 mock_ctx.close().await;
10702 }
10703 }
10704
10705 #[tokio::test(flavor = "multi_thread")]
10706 async fn test_coincident_of_line_end_points() {
10707 let initial_source = "\
10708sketch(on = XY) {
10709 line(start = [var 1, var 2], end = [var 3, var 4])
10710 line(start = [var 5, var 6], end = [var 7, var 8])
10711}
10712";
10713
10714 let program = Program::parse(initial_source).unwrap().0.unwrap();
10715
10716 let mut frontend = FrontendState::new();
10717
10718 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10719 let mock_ctx = ExecutorContext::new_mock(None).await;
10720 let version = Version(0);
10721
10722 frontend.hack_set_program(&ctx, program).await.unwrap();
10723 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10724 let sketch_id = sketch_object.id;
10725 let sketch = expect_sketch(sketch_object);
10726 let point0_id = *sketch.segments.get(1).unwrap();
10727 let point1_id = *sketch.segments.get(3).unwrap();
10728
10729 let constraint = Constraint::Coincident(Coincident {
10730 segments: vec![point0_id.into(), point1_id.into()],
10731 });
10732 let (src_delta, scene_delta) = frontend
10733 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10734 .await
10735 .unwrap();
10736 insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10737 assert_eq!(
10738 scene_delta.new_graph.objects.len(),
10739 9,
10740 "{:#?}",
10741 scene_delta.new_graph.objects
10742 );
10743
10744 ctx.close().await;
10745 mock_ctx.close().await;
10746 }
10747
10748 #[tokio::test(flavor = "multi_thread")]
10749 async fn test_coincident_of_line_point_and_circle_segment() {
10750 let initial_source = "\
10751sketch(on = XY) {
10752 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10753 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10754}
10755";
10756 let program = Program::parse(initial_source).unwrap().0.unwrap();
10757 let mut frontend = FrontendState::new();
10758
10759 let mock_ctx = ExecutorContext::new_mock(None).await;
10760 let version = Version(0);
10761
10762 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10763 frontend.program = program;
10764 frontend.update_state_after_exec(outcome, true);
10765 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10766 let sketch_id = sketch_object.id;
10767 let sketch = expect_sketch(sketch_object);
10768
10769 let circle_id = sketch
10770 .segments
10771 .iter()
10772 .copied()
10773 .find(|seg_id| {
10774 matches!(
10775 &frontend.scene_graph.objects[seg_id.0].kind,
10776 ObjectKind::Segment {
10777 segment: Segment::Circle(_)
10778 }
10779 )
10780 })
10781 .expect("Expected a circle segment in sketch");
10782 let line_id = frontend
10783 .scene_graph
10784 .objects
10785 .iter()
10786 .find_map(|obj| match &obj.kind {
10787 ObjectKind::Segment {
10788 segment: Segment::Line(line),
10789 } if line.owner.is_none() => Some(obj.id),
10790 _ => None,
10791 })
10792 .expect("Expected a standalone line segment in scene graph");
10793
10794 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10795 ObjectKind::Segment {
10796 segment: Segment::Line(line),
10797 } => line.start,
10798 _ => panic!("Expected line segment object"),
10799 };
10800
10801 let constraint = Constraint::Coincident(Coincident {
10802 segments: vec![line_start_point_id.into(), circle_id.into()],
10803 });
10804 let (src_delta, _scene_delta) = frontend
10805 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10806 .await
10807 .unwrap();
10808 insta::assert_snapshot!(
10809 "test_coincident_of_line_point_and_circle_segment",
10810 src_delta.text.as_str()
10811 );
10812
10813 mock_ctx.close().await;
10814 }
10815
10816 #[tokio::test(flavor = "multi_thread")]
10817 async fn test_invalid_coincident_arc_and_line_preserves_state() {
10818 let program = Program::empty();
10826
10827 let mut frontend = FrontendState::new();
10828 frontend.program = program;
10829
10830 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10831 let mock_ctx = ExecutorContext::new_mock(None).await;
10832 let version = Version(0);
10833
10834 let sketch_args = SketchCtor {
10835 on: Plane::Default(PlaneName::Xy),
10836 };
10837 let (_src_delta, _scene_delta, sketch_id) = frontend
10838 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10839 .await
10840 .unwrap();
10841
10842 let arc_ctor = ArcCtor {
10844 start: Point2d {
10845 x: Expr::Var(Number {
10846 value: 0.0,
10847 units: NumericSuffix::Mm,
10848 }),
10849 y: Expr::Var(Number {
10850 value: 0.0,
10851 units: NumericSuffix::Mm,
10852 }),
10853 },
10854 end: Point2d {
10855 x: Expr::Var(Number {
10856 value: 10.0,
10857 units: NumericSuffix::Mm,
10858 }),
10859 y: Expr::Var(Number {
10860 value: 10.0,
10861 units: NumericSuffix::Mm,
10862 }),
10863 },
10864 center: Point2d {
10865 x: Expr::Var(Number {
10866 value: 10.0,
10867 units: NumericSuffix::Mm,
10868 }),
10869 y: Expr::Var(Number {
10870 value: 0.0,
10871 units: NumericSuffix::Mm,
10872 }),
10873 },
10874 direction: None,
10875 construction: None,
10876 };
10877 let (_src_delta, scene_delta) = frontend
10878 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10879 .await
10880 .unwrap();
10881 let arc_id = *scene_delta.new_objects.last().unwrap();
10883
10884 let line_ctor = LineCtor {
10886 start: Point2d {
10887 x: Expr::Var(Number {
10888 value: 20.0,
10889 units: NumericSuffix::Mm,
10890 }),
10891 y: Expr::Var(Number {
10892 value: 0.0,
10893 units: NumericSuffix::Mm,
10894 }),
10895 },
10896 end: Point2d {
10897 x: Expr::Var(Number {
10898 value: 30.0,
10899 units: NumericSuffix::Mm,
10900 }),
10901 y: Expr::Var(Number {
10902 value: 10.0,
10903 units: NumericSuffix::Mm,
10904 }),
10905 },
10906 construction: None,
10907 };
10908 let (_src_delta, scene_delta) = frontend
10909 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10910 .await
10911 .unwrap();
10912 let line_id = *scene_delta.new_objects.last().unwrap();
10914
10915 let constraint = Constraint::Coincident(Coincident {
10918 segments: vec![arc_id.into(), line_id.into()],
10919 });
10920 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10921
10922 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10924
10925 let sketch_object_after =
10928 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10929 let sketch_after = expect_sketch(sketch_object_after);
10930
10931 assert!(
10933 sketch_after.segments.contains(&arc_id),
10934 "Arc segment should still exist after failed constraint"
10935 );
10936 assert!(
10937 sketch_after.segments.contains(&line_id),
10938 "Line segment should still exist after failed constraint"
10939 );
10940
10941 let arc_obj = frontend
10943 .scene_graph
10944 .objects
10945 .get(arc_id.0)
10946 .expect("Arc object should still be accessible");
10947 let line_obj = frontend
10948 .scene_graph
10949 .objects
10950 .get(line_id.0)
10951 .expect("Line object should still be accessible");
10952
10953 match &arc_obj.kind {
10956 ObjectKind::Segment {
10957 segment: Segment::Arc(_),
10958 } => {}
10959 _ => panic!("Arc object should still be an arc segment"),
10960 }
10961 match &line_obj.kind {
10962 ObjectKind::Segment {
10963 segment: Segment::Line(_),
10964 } => {}
10965 _ => panic!("Line object should still be a line segment"),
10966 }
10967
10968 ctx.close().await;
10969 mock_ctx.close().await;
10970 }
10971
10972 #[tokio::test(flavor = "multi_thread")]
10973 async fn test_distance_two_points() {
10974 let initial_source = "\
10975sketch(on = XY) {
10976 point(at = [var 1, var 2])
10977 point(at = [var 3, var 4])
10978}
10979";
10980
10981 let program = Program::parse(initial_source).unwrap().0.unwrap();
10982
10983 let mut frontend = FrontendState::new();
10984
10985 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10986 let mock_ctx = ExecutorContext::new_mock(None).await;
10987 let version = Version(0);
10988
10989 frontend.hack_set_program(&ctx, program).await.unwrap();
10990 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10991 let sketch_id = sketch_object.id;
10992 let sketch = expect_sketch(sketch_object);
10993 let point0_id = *sketch.segments.first().unwrap();
10994 let point1_id = *sketch.segments.get(1).unwrap();
10995
10996 let constraint = Constraint::Distance(Distance {
10997 segments: vec![point0_id.into(), point1_id.into()],
10998 distance: Number {
10999 value: 2.0,
11000 units: NumericSuffix::Mm,
11001 },
11002 label_position: None,
11003 source: Default::default(),
11004 });
11005 let (src_delta, scene_delta) = frontend
11006 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11007 .await
11008 .unwrap();
11009 insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
11010 assert_eq!(
11011 scene_delta.new_graph.objects.len(),
11012 5,
11013 "{:#?}",
11014 scene_delta.new_graph.objects
11015 );
11016
11017 ctx.close().await;
11018 mock_ctx.close().await;
11019 }
11020
11021 #[tokio::test(flavor = "multi_thread")]
11022 async fn test_distance_two_points_with_label() {
11023 let initial_source = "\
11024sketch(on = XY) {
11025 point(at = [var 1, var 2])
11026 point(at = [var 3, var 4])
11027}
11028";
11029
11030 let program = Program::parse(initial_source).unwrap().0.unwrap();
11031
11032 let mut frontend = FrontendState::new();
11033
11034 let mock_ctx = ExecutorContext::new_mock(None).await;
11035 let version = Version(0);
11036
11037 frontend.program = program.clone();
11038 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11039 frontend.update_state_after_exec(outcome, true);
11040 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11041 let sketch_id = sketch_object.id;
11042 let sketch = expect_sketch(sketch_object);
11043 let point0_id = *sketch.segments.first().unwrap();
11044 let point1_id = *sketch.segments.get(1).unwrap();
11045
11046 let label_position = Point2d {
11047 x: Number {
11048 value: 10.0,
11049 units: NumericSuffix::Mm,
11050 },
11051 y: Number {
11052 value: 11.0,
11053 units: NumericSuffix::Mm,
11054 },
11055 };
11056 let constraint = Constraint::Distance(Distance {
11057 segments: vec![point0_id.into(), point1_id.into()],
11058 distance: Number {
11059 value: 2.0,
11060 units: NumericSuffix::Mm,
11061 },
11062 label_position: Some(label_position.clone()),
11063 source: Default::default(),
11064 });
11065 let (src_delta, scene_delta) = frontend
11066 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11067 .await
11068 .unwrap();
11069 insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11070
11071 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11072 let sketch = expect_sketch(sketch_object);
11073 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11074 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11075 panic!("Expected constraint object");
11076 };
11077 let Constraint::Distance(distance) = constraint else {
11078 panic!("Expected distance constraint");
11079 };
11080 assert_eq!(distance.label_position, Some(label_position));
11081
11082 mock_ctx.close().await;
11083 }
11084
11085 #[tokio::test(flavor = "multi_thread")]
11086 async fn test_edit_distance_constraint_label_position() {
11087 let initial_source = "\
11088sketch(on = XY) {
11089 point(at = [var 1, var 2])
11090 point(at = [var 3, var 2])
11091}
11092";
11093
11094 let program = Program::parse(initial_source).unwrap().0.unwrap();
11095
11096 let mut frontend = FrontendState::new();
11097
11098 let mock_ctx = ExecutorContext::new_mock(None).await;
11099 let version = Version(0);
11100
11101 frontend.program = program.clone();
11102 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11103 frontend.update_state_after_exec(outcome, true);
11104 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11105 let sketch_id = sketch_object.id;
11106 let sketch = expect_sketch(sketch_object);
11107 let point0_id = *sketch.segments.first().unwrap();
11108 let point1_id = *sketch.segments.get(1).unwrap();
11109
11110 let constraint = Constraint::Distance(Distance {
11111 segments: vec![point0_id.into(), point1_id.into()],
11112 distance: Number {
11113 value: 2.0,
11114 units: NumericSuffix::Mm,
11115 },
11116 label_position: None,
11117 source: Default::default(),
11118 });
11119 let (_, scene_delta) = frontend
11120 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11121 .await
11122 .unwrap();
11123 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11124 let sketch = expect_sketch(sketch_object);
11125 let constraint_id = sketch.constraints[0];
11126 let label_position = Point2d {
11127 x: Number {
11128 value: 10.0,
11129 units: NumericSuffix::Mm,
11130 },
11131 y: Number {
11132 value: 11.0,
11133 units: NumericSuffix::Mm,
11134 },
11135 };
11136
11137 let (src_delta, scene_delta) = frontend
11138 .edit_distance_constraint_label_position(
11139 &mock_ctx,
11140 version,
11141 sketch_id,
11142 constraint_id,
11143 label_position.clone(),
11144 vec![],
11145 )
11146 .await
11147 .unwrap();
11148 insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11149
11150 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11151 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11152 panic!("Expected constraint object");
11153 };
11154 let Constraint::Distance(distance) = constraint else {
11155 panic!("Expected distance constraint");
11156 };
11157 assert_eq!(distance.label_position, Some(label_position));
11158
11159 mock_ctx.close().await;
11160 }
11161
11162 #[tokio::test(flavor = "multi_thread")]
11163 async fn test_edit_distance_constraint_type_and_value() {
11164 let initial_source = "\
11165sketch(on = XY) {
11166 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11167 distance([line1.start, line1.end]) == 5mm
11168}
11169";
11170
11171 let program = Program::parse(initial_source).unwrap().0.unwrap();
11172 let mut frontend = FrontendState::new();
11173 let mock_ctx = ExecutorContext::new_mock(None).await;
11174 let version = Version(0);
11175
11176 frontend.program = program.clone();
11177 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11178 frontend.update_state_after_exec(outcome, true);
11179 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11180 let sketch_id = sketch_object.id;
11181 let sketch = expect_sketch(sketch_object);
11182 let constraint_id = sketch.constraints[0];
11183 let point0_id = sketch.segments[0];
11184 let point1_id = sketch.segments[1];
11185 let label_position = Point2d {
11186 x: Number {
11187 value: 2.0,
11188 units: NumericSuffix::Mm,
11189 },
11190 y: Number {
11191 value: 5.0,
11192 units: NumericSuffix::Mm,
11193 },
11194 };
11195
11196 let (source_delta, scene_delta) = frontend
11197 .edit_distance_constraint_with_options(
11198 &mock_ctx,
11199 version,
11200 sketch_id,
11201 constraint_id,
11202 Constraint::HorizontalDistance(Distance {
11203 segments: vec![point0_id.into(), point1_id.into()],
11204 distance: Number {
11205 value: 4.0,
11206 units: NumericSuffix::Mm,
11207 },
11208 label_position: Some(label_position.clone()),
11209 source: Default::default(),
11210 }),
11211 EditConstraintOptions {
11212 commit_solved_initial_guesses: false,
11213 },
11214 )
11215 .await
11216 .unwrap();
11217 assert_eq!(
11218 source_delta.text,
11219 "\
11220sketch(on = XY) {
11221 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11222 horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11223}
11224"
11225 );
11226
11227 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11228 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11229 panic!("Expected constraint object");
11230 };
11231 let Constraint::HorizontalDistance(distance) = constraint else {
11232 panic!("Expected horizontal distance constraint");
11233 };
11234 assert_eq!(distance.distance.value, 4.0);
11235 assert_eq!(distance.label_position, Some(label_position));
11236
11237 mock_ctx.close().await;
11238 }
11239
11240 #[tokio::test(flavor = "multi_thread")]
11241 async fn test_edit_angle_constraint_label_position() {
11242 let initial_source = "\
11243sketch(on = XY) {
11244 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11245 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11246 angle([line1, line2]) == 60deg
11247}
11248";
11249
11250 let program = Program::parse(initial_source).unwrap().0.unwrap();
11251 let mut frontend = FrontendState::new();
11252 let mock_ctx = ExecutorContext::new_mock(None).await;
11253 let version = Version(0);
11254
11255 frontend.program = program.clone();
11256 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11257 frontend.update_state_after_exec(outcome, true);
11258 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11259 let sketch_id = sketch_object.id;
11260 let sketch = expect_sketch(sketch_object);
11261 let constraint_id = sketch.constraints[0];
11262 let label_position = Point2d {
11263 x: Number {
11264 value: 10.0,
11265 units: NumericSuffix::Mm,
11266 },
11267 y: Number {
11268 value: 11.0,
11269 units: NumericSuffix::Mm,
11270 },
11271 };
11272
11273 let (src_delta, scene_delta) = frontend
11274 .edit_distance_constraint_label_position(
11275 &mock_ctx,
11276 version,
11277 sketch_id,
11278 constraint_id,
11279 label_position.clone(),
11280 vec![],
11281 )
11282 .await
11283 .unwrap();
11284 assert_eq!(
11285 src_delta.text.as_str(),
11286 "\
11287sketch(on = XY) {
11288 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11289 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11290 angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11291}
11292"
11293 );
11294
11295 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11296 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11297 panic!("Expected constraint object");
11298 };
11299 let Constraint::Angle(angle) = constraint else {
11300 panic!("Expected angle constraint");
11301 };
11302 assert_eq!(angle.label_position, Some(label_position));
11303
11304 mock_ctx.close().await;
11305 }
11306
11307 #[tokio::test(flavor = "multi_thread")]
11308 async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11309 let initial_source = "\
11310sketch(on = XY) {
11311 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11312 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11313 60deg == angleDimension(lines = [line1, line2], sector = 1)
11314}
11315";
11316
11317 let program = Program::parse(initial_source).unwrap().0.unwrap();
11318 let mut frontend = FrontendState::new();
11319 let mock_ctx = ExecutorContext::new_mock(None).await;
11320 let version = Version(0);
11321
11322 frontend.program = program.clone();
11323 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11324 frontend.update_state_after_exec(outcome, true);
11325 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11326 let sketch_id = sketch_object.id;
11327 let sketch = expect_sketch(sketch_object);
11328 let constraint_id = sketch.constraints[0];
11329 let label_position = Point2d {
11330 x: Number {
11331 value: 10.0,
11332 units: NumericSuffix::Mm,
11333 },
11334 y: Number {
11335 value: 11.0,
11336 units: NumericSuffix::Mm,
11337 },
11338 };
11339
11340 let (src_delta, scene_delta) = frontend
11341 .edit_distance_constraint_label_position(
11342 &mock_ctx,
11343 version,
11344 sketch_id,
11345 constraint_id,
11346 label_position.clone(),
11347 vec![],
11348 )
11349 .await
11350 .unwrap();
11351 assert_eq!(
11352 src_delta.text.as_str(),
11353 "\
11354sketch(on = XY) {
11355 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11356 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11357 60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11358}
11359"
11360 );
11361
11362 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11363 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11364 panic!("Expected constraint object");
11365 };
11366 let Constraint::Angle(angle) = constraint else {
11367 panic!("Expected angle constraint");
11368 };
11369 assert_eq!(angle.label_position, Some(label_position));
11370
11371 mock_ctx.close().await;
11372 }
11373
11374 #[tokio::test(flavor = "multi_thread")]
11375 async fn test_edit_angle_constraint() {
11376 let initial_source = "\
11377sketch(on = XY) {
11378 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11379 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11380 angle([line1, line2]) == 60deg
11381}
11382";
11383
11384 let program = Program::parse(initial_source).unwrap().0.unwrap();
11385 let mut frontend = FrontendState::new();
11386 let mock_ctx = ExecutorContext::new_mock(None).await;
11387 let version = Version(0);
11388
11389 frontend.program = program.clone();
11390 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11391 frontend.update_state_after_exec(outcome, true);
11392 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11393 let sketch_id = sketch_object.id;
11394 let sketch = expect_sketch(sketch_object);
11395 let constraint_id = sketch.constraints[0];
11396 let line1_id = *sketch.segments.get(2).unwrap();
11397 let line2_id = *sketch.segments.get(5).unwrap();
11398 let label_position = Point2d {
11399 x: Number {
11400 value: 10.0,
11401 units: NumericSuffix::Mm,
11402 },
11403 y: Number {
11404 value: 11.0,
11405 units: NumericSuffix::Mm,
11406 },
11407 };
11408
11409 let (src_delta, scene_delta) = frontend
11410 .edit_angle_constraint_with_options(
11411 &mock_ctx,
11412 version,
11413 sketch_id,
11414 constraint_id,
11415 Angle {
11416 lines: vec![line2_id, line1_id],
11417 angle: Number {
11418 value: 60.0,
11419 units: NumericSuffix::Deg,
11420 },
11421 sector: Some(3),
11422 inverse: Some(false),
11423 label_position: Some(label_position.clone()),
11424 source: Default::default(),
11425 },
11426 EditConstraintOptions {
11427 commit_solved_initial_guesses: false,
11428 },
11429 )
11430 .await
11431 .unwrap();
11432 assert_eq!(
11433 src_delta.text.as_str(),
11434 "\
11435sketch(on = XY) {
11436 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11437 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11438 angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11439}
11440"
11441 );
11442
11443 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11444 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11445 panic!("Expected constraint object");
11446 };
11447 let Constraint::Angle(angle) = constraint else {
11448 panic!("Expected angle constraint");
11449 };
11450 assert_eq!(angle.lines, vec![line2_id, line1_id]);
11451 assert_eq!(angle.sector, Some(3));
11452 assert_eq!(angle.inverse, Some(false));
11453 assert_eq!(angle.label_position, Some(label_position));
11454
11455 mock_ctx.close().await;
11456 }
11457
11458 #[tokio::test(flavor = "multi_thread")]
11459 async fn test_edit_angle_constraint_with_call_on_right() {
11460 let initial_source = "\
11461sketch(on = XY) {
11462 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11463 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11464 60deg == angle([line1, line2])
11465}
11466";
11467
11468 let program = Program::parse(initial_source).unwrap().0.unwrap();
11469 let mut frontend = FrontendState::new();
11470 let mock_ctx = ExecutorContext::new_mock(None).await;
11471 let version = Version(0);
11472
11473 frontend.program = program.clone();
11474 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11475 frontend.update_state_after_exec(outcome, true);
11476 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11477 let sketch_id = sketch_object.id;
11478 let sketch = expect_sketch(sketch_object);
11479 let constraint_id = sketch.constraints[0];
11480 let line1_id = *sketch.segments.get(2).unwrap();
11481 let line2_id = *sketch.segments.get(5).unwrap();
11482
11483 let (src_delta, _) = frontend
11484 .edit_angle_constraint_with_options(
11485 &mock_ctx,
11486 version,
11487 sketch_id,
11488 constraint_id,
11489 Angle {
11490 lines: vec![line2_id, line1_id],
11491 angle: Number {
11492 value: 60.0,
11493 units: NumericSuffix::Deg,
11494 },
11495 sector: Some(3),
11496 inverse: Some(false),
11497 label_position: None,
11498 source: Default::default(),
11499 },
11500 EditConstraintOptions {
11501 commit_solved_initial_guesses: false,
11502 },
11503 )
11504 .await
11505 .unwrap();
11506 assert_eq!(
11507 src_delta.text.as_str(),
11508 "\
11509sketch(on = XY) {
11510 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11511 line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11512 60deg == angleDimension(lines = [line2, line1], sector = 3)
11513}
11514"
11515 );
11516
11517 mock_ctx.close().await;
11518 }
11519
11520 #[tokio::test(flavor = "multi_thread")]
11521 async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11522 let initial_source = "\
11523@settings(kclVersion = 2.0)
11524
11525sketch001 = sketch(on = XZ) {
11526 line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11527 line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11528 distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11529 vertical([line1.start, ORIGIN])
11530}
11531";
11532
11533 let program = Program::parse(initial_source).unwrap().0.unwrap();
11534 let mut frontend = FrontendState::new();
11535 let mock_ctx = ExecutorContext::new_mock(None).await;
11536 let version = Version(0);
11537
11538 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11539 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11540 let sketch_id = sketch_object.id;
11541 let sketch = expect_sketch(sketch_object);
11542 let constraint_id = sketch
11543 .constraints
11544 .iter()
11545 .copied()
11546 .find(|constraint_id| {
11547 matches!(
11548 frontend.scene_graph.objects[constraint_id.0].kind,
11549 ObjectKind::Constraint {
11550 constraint: Constraint::Distance(_)
11551 }
11552 )
11553 })
11554 .unwrap();
11555 let line1_id = sketch
11556 .segments
11557 .iter()
11558 .copied()
11559 .find(|segment_id| {
11560 matches!(
11561 frontend.scene_graph.objects[segment_id.0].kind,
11562 ObjectKind::Segment {
11563 segment: Segment::Line(_)
11564 }
11565 )
11566 })
11567 .unwrap();
11568 let label_position = Point2d {
11569 x: Number {
11570 value: 7.0,
11571 units: NumericSuffix::Mm,
11572 },
11573 y: Number {
11574 value: 9.0,
11575 units: NumericSuffix::Mm,
11576 },
11577 };
11578
11579 let (source_delta, scene_delta) = frontend
11580 .edit_segments_with_options(
11581 &mock_ctx,
11582 version,
11583 sketch_id,
11584 vec![ExistingSegmentCtor {
11585 id: line1_id,
11586 ctor: SegmentCtor::Line(LineCtor {
11587 start: point_expr_mm(2.0, 15.55),
11588 end: point_expr_mm(-4.03, 11.51),
11589 construction: None,
11590 }),
11591 }],
11592 EditSegmentsOptions {
11593 anchor_segment_ids: Some(vec![]),
11594 drag_anchors: vec![SegmentDragAnchor {
11595 segment_id: line1_id,
11596 target: label_position.clone(),
11597 }],
11598 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11599 constraint_id,
11600 label_position: label_position.clone(),
11601 }],
11602 commit_solved_initial_guesses: true,
11603 },
11604 )
11605 .await
11606 .unwrap();
11607
11608 assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11609 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11610 let ObjectKind::Constraint {
11611 constraint: Constraint::Distance(distance),
11612 } = &constraint_object.kind
11613 else {
11614 panic!("Expected distance constraint object");
11615 };
11616 assert_eq!(distance.label_position, Some(label_position));
11617
11618 let snapped_label_position = Point2d {
11619 x: Number {
11620 value: 8.0,
11621 units: NumericSuffix::Mm,
11622 },
11623 y: Number {
11624 value: 10.0,
11625 units: NumericSuffix::Mm,
11626 },
11627 };
11628 let (source_delta, scene_delta) = frontend
11629 .edit_segments_with_options(
11630 &mock_ctx,
11631 version,
11632 sketch_id,
11633 vec![],
11634 EditSegmentsOptions {
11635 anchor_segment_ids: Some(vec![line1_id]),
11636 drag_anchors: vec![],
11637 constraint_label_edits: vec![ConstraintLabelPositionEdit {
11638 constraint_id,
11639 label_position: snapped_label_position.clone(),
11640 }],
11641 commit_solved_initial_guesses: true,
11642 },
11643 )
11644 .await
11645 .unwrap();
11646
11647 assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11648 let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11649 let ObjectKind::Constraint {
11650 constraint: Constraint::Distance(distance),
11651 } = &constraint_object.kind
11652 else {
11653 panic!("Expected distance constraint object");
11654 };
11655 assert_eq!(distance.label_position, Some(snapped_label_position));
11656
11657 mock_ctx.close().await;
11658 }
11659
11660 #[tokio::test(flavor = "multi_thread")]
11661 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11662 let initial_source = "\
11663sketch(on = XY) {
11664 point1 = point(at = [var 0mm, var 0mm])
11665 point2 = point(at = [var 10mm, var 0mm])
11666 distance([point1, point2]) == 5mm
11667}
11668";
11669
11670 let program = Program::parse(initial_source).unwrap().0.unwrap();
11671 let mut frontend = FrontendState::new();
11672 let mock_ctx = ExecutorContext::new_mock(None).await;
11673 let version = Version(0);
11674
11675 frontend.program = program.clone();
11676 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11677 frontend.update_state_after_exec(outcome, true);
11678 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11679 let sketch_id = sketch_object.id;
11680 let sketch = expect_sketch(sketch_object);
11681 let point0_id = sketch.segments[0];
11682 let point1_id = sketch.segments[1];
11683 let constraint_id = sketch.constraints[0];
11684
11685 let edited_segments = vec![ExistingSegmentCtor {
11686 id: point0_id,
11687 ctor: SegmentCtor::Point(PointCtor {
11688 position: Point2d {
11689 x: Expr::Var(Number {
11690 value: 2.0,
11691 units: NumericSuffix::Mm,
11692 }),
11693 y: Expr::Var(Number {
11694 value: 1.0,
11695 units: NumericSuffix::Mm,
11696 }),
11697 },
11698 }),
11699 }];
11700 let (_, scene_delta) = frontend
11701 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11702 .await
11703 .unwrap();
11704 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11705 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11706
11707 let label_position = Point2d {
11708 x: Number {
11709 value: 3.0,
11710 units: NumericSuffix::Mm,
11711 },
11712 y: Number {
11713 value: 4.0,
11714 units: NumericSuffix::Mm,
11715 },
11716 };
11717 let (_, scene_delta) = frontend
11718 .edit_distance_constraint_label_position(
11719 &mock_ctx,
11720 version,
11721 sketch_id,
11722 constraint_id,
11723 label_position,
11724 vec![point0_id],
11725 )
11726 .await
11727 .unwrap();
11728
11729 assert_point_position_close(
11730 point_position(&scene_delta.new_graph, point0_id),
11731 point0_after_segment_edit,
11732 );
11733 assert_point_position_close(
11734 point_position(&scene_delta.new_graph, point1_id),
11735 point1_after_segment_edit,
11736 );
11737
11738 mock_ctx.close().await;
11739 }
11740
11741 #[tokio::test(flavor = "multi_thread")]
11742 async fn test_distance_point_line() {
11743 let initial_source = "\
11744sketch(on = XY) {
11745 point(at = [var 0, var 5])
11746 line(start = [var 0, var 0], end = [var 10, var 0])
11747}
11748";
11749
11750 let program = Program::parse(initial_source).unwrap().0.unwrap();
11751
11752 let mut frontend = FrontendState::new();
11753
11754 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11755 let mock_ctx = ExecutorContext::new_mock(None).await;
11756 let version = Version(0);
11757
11758 frontend.hack_set_program(&ctx, program).await.unwrap();
11759 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11760 let sketch_id = sketch_object.id;
11761 let sketch = expect_sketch(sketch_object);
11762 let point_id = *sketch.segments.first().unwrap();
11763 let line_id = *sketch
11764 .segments
11765 .iter()
11766 .find(|segment_id| {
11767 matches!(
11768 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11769 Some(ObjectKind::Segment {
11770 segment: Segment::Line(_)
11771 })
11772 )
11773 })
11774 .unwrap();
11775
11776 let label_position = Point2d {
11777 x: Number {
11778 value: 10.0,
11779 units: NumericSuffix::Mm,
11780 },
11781 y: Number {
11782 value: 11.0,
11783 units: NumericSuffix::Mm,
11784 },
11785 };
11786 let constraint = Constraint::Distance(Distance {
11787 segments: vec![point_id.into(), line_id.into()],
11788 distance: Number {
11789 value: 5.0,
11790 units: NumericSuffix::Mm,
11791 },
11792 label_position: Some(label_position.clone()),
11793 source: Default::default(),
11794 });
11795 let (src_delta, scene_delta) = frontend
11796 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11797 .await
11798 .unwrap();
11799 insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11800 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11801 let sketch = expect_sketch(sketch_object);
11802 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11803 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11804 panic!("Expected constraint object");
11805 };
11806 let Constraint::Distance(distance) = constraint else {
11807 panic!("Expected distance constraint");
11808 };
11809 assert_eq!(distance.label_position, Some(label_position));
11810
11811 ctx.close().await;
11812 mock_ctx.close().await;
11813 }
11814
11815 #[tokio::test(flavor = "multi_thread")]
11816 async fn test_distance_point_arc() {
11817 let initial_source = "\
11818sketch(on = XY) {
11819 point(at = [var 0, var 8])
11820 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11821}
11822";
11823
11824 let program = Program::parse(initial_source).unwrap().0.unwrap();
11825
11826 let mut frontend = FrontendState::new();
11827
11828 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11829 let mock_ctx = ExecutorContext::new_mock(None).await;
11830 let version = Version(0);
11831
11832 frontend.hack_set_program(&ctx, program).await.unwrap();
11833 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11834 let sketch_id = sketch_object.id;
11835 let sketch = expect_sketch(sketch_object);
11836 let point_id = *sketch.segments.first().unwrap();
11837 let arc_id = *sketch
11838 .segments
11839 .iter()
11840 .find(|segment_id| {
11841 matches!(
11842 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11843 Some(ObjectKind::Segment {
11844 segment: Segment::Arc(_)
11845 })
11846 )
11847 })
11848 .unwrap();
11849
11850 let constraint = Constraint::Distance(Distance {
11851 segments: vec![point_id.into(), arc_id.into()],
11852 distance: Number {
11853 value: 3.0,
11854 units: NumericSuffix::Mm,
11855 },
11856 label_position: None,
11857 source: Default::default(),
11858 });
11859 let (src_delta, _scene_delta) = frontend
11860 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11861 .await
11862 .unwrap();
11863 insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11864
11865 ctx.close().await;
11866 mock_ctx.close().await;
11867 }
11868
11869 #[tokio::test(flavor = "multi_thread")]
11870 async fn test_distance_arc_origin() {
11871 let initial_source = "\
11872sketch001 = sketch(on = XY) {
11873 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11874}
11875";
11876
11877 let program = Program::parse(initial_source).unwrap().0.unwrap();
11878
11879 let mut frontend = FrontendState::new();
11880
11881 let mock_ctx = ExecutorContext::new_mock(None).await;
11882 let version = Version(0);
11883
11884 frontend.program = program.clone();
11885 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11886 frontend.update_state_after_exec(outcome, true);
11887 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11888 let sketch_id = sketch_object.id;
11889 let sketch = expect_sketch(sketch_object);
11890 let arc_id = *sketch
11891 .segments
11892 .iter()
11893 .find(|segment_id| {
11894 matches!(
11895 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11896 Some(ObjectKind::Segment {
11897 segment: Segment::Arc(_)
11898 })
11899 )
11900 })
11901 .unwrap();
11902
11903 let constraint = Constraint::Distance(Distance {
11904 segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11905 distance: Number {
11906 value: 3.0,
11907 units: NumericSuffix::Mm,
11908 },
11909 label_position: None,
11910 source: Default::default(),
11911 });
11912 let (src_delta, _scene_delta) = frontend
11913 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11914 .await
11915 .unwrap();
11916 insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11917
11918 mock_ctx.close().await;
11919 }
11920
11921 #[tokio::test(flavor = "multi_thread")]
11922 async fn test_distance_line_origin() {
11923 let initial_source = "\
11924sketch(on = XY) {
11925 line(start = [var 5, var 0], end = [var 5, var 10])
11926}
11927";
11928
11929 let program = Program::parse(initial_source).unwrap().0.unwrap();
11930
11931 let mut frontend = FrontendState::new();
11932
11933 let mock_ctx = ExecutorContext::new_mock(None).await;
11934 let version = Version(0);
11935
11936 frontend.program = program.clone();
11937 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11938 frontend.update_state_after_exec(outcome, true);
11939 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11940 let sketch_id = sketch_object.id;
11941 let sketch = expect_sketch(sketch_object);
11942 let line_id = *sketch
11943 .segments
11944 .iter()
11945 .find(|segment_id| {
11946 matches!(
11947 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11948 Some(ObjectKind::Segment {
11949 segment: Segment::Line(_)
11950 })
11951 )
11952 })
11953 .unwrap();
11954
11955 let constraint = Constraint::Distance(Distance {
11956 segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11957 distance: Number {
11958 value: 5.0,
11959 units: NumericSuffix::Mm,
11960 },
11961 label_position: None,
11962 source: Default::default(),
11963 });
11964 let (src_delta, _scene_delta) = frontend
11965 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11966 .await
11967 .unwrap();
11968 insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11969
11970 mock_ctx.close().await;
11971 }
11972
11973 #[tokio::test(flavor = "multi_thread")]
11974 async fn test_distance_line_circle() {
11975 let initial_source = "\
11976sketch(on = XY) {
11977 line(start = [var -10, var 8], end = [var 10, var 8])
11978 circle(start = [var 5, var 0], center = [var 0, var 0])
11979}
11980";
11981
11982 let program = Program::parse(initial_source).unwrap().0.unwrap();
11983
11984 let mut frontend = FrontendState::new();
11985
11986 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11987 let mock_ctx = ExecutorContext::new_mock(None).await;
11988 let version = Version(0);
11989
11990 frontend.hack_set_program(&ctx, program).await.unwrap();
11991 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11992 let sketch_id = sketch_object.id;
11993 let sketch = expect_sketch(sketch_object);
11994 let line_id = *sketch
11995 .segments
11996 .iter()
11997 .find(|segment_id| {
11998 matches!(
11999 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12000 Some(ObjectKind::Segment {
12001 segment: Segment::Line(_)
12002 })
12003 )
12004 })
12005 .unwrap();
12006 let circle_id = *sketch
12007 .segments
12008 .iter()
12009 .find(|segment_id| {
12010 matches!(
12011 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12012 Some(ObjectKind::Segment {
12013 segment: Segment::Circle(_)
12014 })
12015 )
12016 })
12017 .unwrap();
12018
12019 let constraint = Constraint::Distance(Distance {
12020 segments: vec![line_id.into(), circle_id.into()],
12021 distance: Number {
12022 value: 3.0,
12023 units: NumericSuffix::Mm,
12024 },
12025 label_position: None,
12026 source: Default::default(),
12027 });
12028 let (src_delta, _scene_delta) = frontend
12029 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12030 .await
12031 .unwrap();
12032 insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
12033
12034 ctx.close().await;
12035 mock_ctx.close().await;
12036 }
12037
12038 #[tokio::test(flavor = "multi_thread")]
12039 async fn test_distance_circle_arc() {
12040 let initial_source = "\
12041sketch(on = XY) {
12042 circle(start = [var 5, var 0], center = [var 0, var 0])
12043 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
12044}
12045";
12046
12047 let program = Program::parse(initial_source).unwrap().0.unwrap();
12048
12049 let mut frontend = FrontendState::new();
12050
12051 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12052 let mock_ctx = ExecutorContext::new_mock(None).await;
12053 let version = Version(0);
12054
12055 frontend.hack_set_program(&ctx, program).await.unwrap();
12056 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12057 let sketch_id = sketch_object.id;
12058 let sketch = expect_sketch(sketch_object);
12059 let circle_id = *sketch
12060 .segments
12061 .iter()
12062 .find(|segment_id| {
12063 matches!(
12064 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12065 Some(ObjectKind::Segment {
12066 segment: Segment::Circle(_)
12067 })
12068 )
12069 })
12070 .unwrap();
12071 let arc_id = *sketch
12072 .segments
12073 .iter()
12074 .find(|segment_id| {
12075 matches!(
12076 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12077 Some(ObjectKind::Segment {
12078 segment: Segment::Arc(_)
12079 })
12080 )
12081 })
12082 .unwrap();
12083
12084 let constraint = Constraint::Distance(Distance {
12085 segments: vec![circle_id.into(), arc_id.into()],
12086 distance: Number {
12087 value: 3.0,
12088 units: NumericSuffix::Mm,
12089 },
12090 label_position: None,
12091 source: Default::default(),
12092 });
12093 let (src_delta, _scene_delta) = frontend
12094 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12095 .await
12096 .unwrap();
12097 insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12098
12099 ctx.close().await;
12100 mock_ctx.close().await;
12101 }
12102
12103 #[tokio::test(flavor = "multi_thread")]
12104 async fn test_distance_parallel_lines() {
12105 let initial_source = "\
12106sketch(on = XY) {
12107 line(start = [var 0, var 0], end = [var 10, var 0])
12108 line(start = [var 0, var 5], end = [var 10, var 5])
12109}
12110";
12111
12112 let program = Program::parse(initial_source).unwrap().0.unwrap();
12113
12114 let mut frontend = FrontendState::new();
12115
12116 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12117 let mock_ctx = ExecutorContext::new_mock(None).await;
12118 let version = Version(0);
12119
12120 frontend.hack_set_program(&ctx, program).await.unwrap();
12121 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12122 let sketch_id = sketch_object.id;
12123 let sketch = expect_sketch(sketch_object);
12124 let line_ids = sketch
12125 .segments
12126 .iter()
12127 .copied()
12128 .filter(|segment_id| {
12129 matches!(
12130 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12131 Some(ObjectKind::Segment {
12132 segment: Segment::Line(_)
12133 })
12134 )
12135 })
12136 .collect::<Vec<_>>();
12137
12138 let constraint = Constraint::Distance(Distance {
12139 segments: vec![line_ids[0].into(), line_ids[1].into()],
12140 distance: Number {
12141 value: 5.0,
12142 units: NumericSuffix::Mm,
12143 },
12144 label_position: None,
12145 source: Default::default(),
12146 });
12147 let (src_delta, _scene_delta) = frontend
12148 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12149 .await
12150 .unwrap();
12151 insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12152
12153 ctx.close().await;
12154 mock_ctx.close().await;
12155 }
12156
12157 #[tokio::test(flavor = "multi_thread")]
12158 async fn test_distance_non_parallel_lines_lowers_to_distance() {
12159 let initial_source = "\
12164sketch(on = XY) {
12165 line(start = [var 0, var 0], end = [var 10, var 0])
12166 line(start = [var 0, var 0], end = [var 10, var 10])
12167}
12168";
12169
12170 let program = Program::parse(initial_source).unwrap().0.unwrap();
12171
12172 let mut frontend = FrontendState::new();
12173
12174 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12175 let mock_ctx = ExecutorContext::new_mock(None).await;
12176 let version = Version(0);
12177
12178 frontend.hack_set_program(&ctx, program).await.unwrap();
12179 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12180 let sketch_id = sketch_object.id;
12181 let sketch = expect_sketch(sketch_object);
12182 let line_ids = sketch
12183 .segments
12184 .iter()
12185 .copied()
12186 .filter(|segment_id| {
12187 matches!(
12188 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12189 Some(ObjectKind::Segment {
12190 segment: Segment::Line(_)
12191 })
12192 )
12193 })
12194 .collect::<Vec<_>>();
12195
12196 let constraint = Constraint::Distance(Distance {
12197 segments: vec![line_ids[0].into(), line_ids[1].into()],
12198 distance: Number {
12199 value: 5.0,
12200 units: NumericSuffix::Mm,
12201 },
12202 label_position: None,
12203 source: Default::default(),
12204 });
12205 let (src_delta, _scene_delta) = frontend
12206 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12207 .await
12208 .unwrap();
12209 insta::assert_snapshot!(
12210 "test_distance_non_parallel_lines_lowers_to_distance",
12211 src_delta.text.as_str()
12212 );
12213
12214 ctx.close().await;
12215 mock_ctx.close().await;
12216 }
12217
12218 #[tokio::test(flavor = "multi_thread")]
12219 async fn test_horizontal_distance_two_points() {
12220 let initial_source = "\
12221sketch(on = XY) {
12222 point(at = [var 1, var 2])
12223 point(at = [var 3, var 4])
12224}
12225";
12226
12227 let program = Program::parse(initial_source).unwrap().0.unwrap();
12228
12229 let mut frontend = FrontendState::new();
12230
12231 let mock_ctx = ExecutorContext::new_mock(None).await;
12232 let version = Version(0);
12233
12234 frontend.program = program.clone();
12235 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12236 frontend.update_state_after_exec(outcome, true);
12237 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12238 let sketch_id = sketch_object.id;
12239 let sketch = expect_sketch(sketch_object);
12240 let point0_id = *sketch.segments.first().unwrap();
12241 let point1_id = *sketch.segments.get(1).unwrap();
12242 let label_position = Point2d {
12243 x: Number {
12244 value: 10.0,
12245 units: NumericSuffix::Mm,
12246 },
12247 y: Number {
12248 value: 11.0,
12249 units: NumericSuffix::Mm,
12250 },
12251 };
12252
12253 let constraint = Constraint::HorizontalDistance(Distance {
12254 segments: vec![point0_id.into(), point1_id.into()],
12255 distance: Number {
12256 value: 2.0,
12257 units: NumericSuffix::Mm,
12258 },
12259 label_position: Some(label_position.clone()),
12260 source: Default::default(),
12261 });
12262 let (src_delta, scene_delta) = frontend
12263 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12264 .await
12265 .unwrap();
12266 insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12267 assert_eq!(
12268 scene_delta.new_graph.objects.len(),
12269 5,
12270 "{:#?}",
12271 scene_delta.new_graph.objects
12272 );
12273 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12274 let sketch = expect_sketch(sketch_object);
12275 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12276 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12277 panic!("Expected constraint object");
12278 };
12279 let Constraint::HorizontalDistance(distance) = constraint else {
12280 panic!("Expected horizontal distance constraint");
12281 };
12282 assert_eq!(distance.label_position, Some(label_position));
12283
12284 mock_ctx.close().await;
12285 }
12286
12287 #[tokio::test(flavor = "multi_thread")]
12288 async fn test_radius_single_arc_segment() {
12289 let initial_source = "\
12290sketch(on = XY) {
12291 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12292}
12293";
12294
12295 let program = Program::parse(initial_source).unwrap().0.unwrap();
12296
12297 let mut frontend = FrontendState::new();
12298
12299 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12300 let mock_ctx = ExecutorContext::new_mock(None).await;
12301 let version = Version(0);
12302
12303 frontend.hack_set_program(&ctx, program).await.unwrap();
12304 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12305 let sketch_id = sketch_object.id;
12306 let sketch = expect_sketch(sketch_object);
12307 let arc_id = sketch
12309 .segments
12310 .iter()
12311 .find(|&seg_id| {
12312 let obj = frontend.scene_graph.objects.get(seg_id.0);
12313 matches!(
12314 obj.map(|o| &o.kind),
12315 Some(ObjectKind::Segment {
12316 segment: Segment::Arc(_)
12317 })
12318 )
12319 })
12320 .unwrap();
12321
12322 let constraint = Constraint::Radius(Radius {
12323 arc: *arc_id,
12324 radius: Number {
12325 value: 5.0,
12326 units: NumericSuffix::Mm,
12327 },
12328 label_position: None,
12329 source: Default::default(),
12330 });
12331 let (src_delta, scene_delta) = frontend
12332 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12333 .await
12334 .unwrap();
12335 insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12336 assert_eq!(
12337 scene_delta.new_graph.objects.len(),
12338 7, "{:#?}",
12340 scene_delta.new_graph.objects
12341 );
12342
12343 ctx.close().await;
12344 mock_ctx.close().await;
12345 }
12346
12347 #[tokio::test(flavor = "multi_thread")]
12348 async fn test_radius_single_arc_segment_with_label_position() {
12349 let initial_source = "\
12350sketch(on = XY) {
12351 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12352}
12353";
12354
12355 let program = Program::parse(initial_source).unwrap().0.unwrap();
12356 let mut frontend = FrontendState::new();
12357 let mock_ctx = ExecutorContext::new_mock(None).await;
12358 let version = Version(0);
12359
12360 frontend.program = program.clone();
12361 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12362 frontend.update_state_after_exec(outcome, true);
12363 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12364 let sketch_id = sketch_object.id;
12365 let sketch = expect_sketch(sketch_object);
12366 let arc_id = sketch
12367 .segments
12368 .iter()
12369 .find(|&seg_id| {
12370 let obj = frontend.scene_graph.objects.get(seg_id.0);
12371 matches!(
12372 obj.map(|o| &o.kind),
12373 Some(ObjectKind::Segment {
12374 segment: Segment::Arc(_)
12375 })
12376 )
12377 })
12378 .unwrap();
12379
12380 let label_position = Point2d {
12381 x: Number {
12382 value: 10.0,
12383 units: NumericSuffix::Mm,
12384 },
12385 y: Number {
12386 value: 11.0,
12387 units: NumericSuffix::Mm,
12388 },
12389 };
12390 let constraint = Constraint::Radius(Radius {
12391 arc: *arc_id,
12392 radius: Number {
12393 value: 5.0,
12394 units: NumericSuffix::Mm,
12395 },
12396 label_position: Some(label_position.clone()),
12397 source: Default::default(),
12398 });
12399 let (src_delta, scene_delta) = frontend
12400 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12401 .await
12402 .unwrap();
12403 insta::assert_snapshot!(
12404 "test_radius_single_arc_segment_with_label_position",
12405 src_delta.text.as_str()
12406 );
12407
12408 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12409 let sketch = expect_sketch(sketch_object);
12410 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12411 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12412 panic!("Expected constraint object");
12413 };
12414 let Constraint::Radius(radius) = constraint else {
12415 panic!("Expected radius constraint");
12416 };
12417 assert_eq!(radius.label_position, Some(label_position));
12418
12419 mock_ctx.close().await;
12420 }
12421
12422 #[tokio::test(flavor = "multi_thread")]
12423 async fn test_edit_radius_constraint_label_position() {
12424 let initial_source = "\
12425sketch(on = XY) {
12426 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12427 radius(arc1) == 5mm
12428}
12429";
12430
12431 let program = Program::parse(initial_source).unwrap().0.unwrap();
12432 let mut frontend = FrontendState::new();
12433 let mock_ctx = ExecutorContext::new_mock(None).await;
12434 let version = Version(0);
12435
12436 frontend.program = program.clone();
12437 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12438 frontend.update_state_after_exec(outcome, true);
12439 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12440 let sketch_id = sketch_object.id;
12441 let sketch = expect_sketch(sketch_object);
12442 let constraint_id = sketch.constraints[0];
12443 let label_position = Point2d {
12444 x: Number {
12445 value: 10.0,
12446 units: NumericSuffix::Mm,
12447 },
12448 y: Number {
12449 value: 11.0,
12450 units: NumericSuffix::Mm,
12451 },
12452 };
12453
12454 let (src_delta, scene_delta) = frontend
12455 .edit_distance_constraint_label_position(
12456 &mock_ctx,
12457 version,
12458 sketch_id,
12459 constraint_id,
12460 label_position.clone(),
12461 vec![],
12462 )
12463 .await
12464 .unwrap();
12465 insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12466
12467 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12468 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12469 panic!("Expected constraint object");
12470 };
12471 let Constraint::Radius(radius) = constraint else {
12472 panic!("Expected radius constraint");
12473 };
12474 assert_eq!(radius.label_position, Some(label_position));
12475
12476 mock_ctx.close().await;
12477 }
12478
12479 #[tokio::test(flavor = "multi_thread")]
12480 async fn test_vertical_distance_two_points() {
12481 let initial_source = "\
12482sketch(on = XY) {
12483 point(at = [var 1, var 2])
12484 point(at = [var 3, var 4])
12485}
12486";
12487
12488 let program = Program::parse(initial_source).unwrap().0.unwrap();
12489
12490 let mut frontend = FrontendState::new();
12491
12492 let mock_ctx = ExecutorContext::new_mock(None).await;
12493 let version = Version(0);
12494
12495 frontend.program = program.clone();
12496 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12497 frontend.update_state_after_exec(outcome, true);
12498 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12499 let sketch_id = sketch_object.id;
12500 let sketch = expect_sketch(sketch_object);
12501 let point0_id = *sketch.segments.first().unwrap();
12502 let point1_id = *sketch.segments.get(1).unwrap();
12503 let label_position = Point2d {
12504 x: Number {
12505 value: 10.0,
12506 units: NumericSuffix::Mm,
12507 },
12508 y: Number {
12509 value: 11.0,
12510 units: NumericSuffix::Mm,
12511 },
12512 };
12513
12514 let constraint = Constraint::VerticalDistance(Distance {
12515 segments: vec![point0_id.into(), point1_id.into()],
12516 distance: Number {
12517 value: 2.0,
12518 units: NumericSuffix::Mm,
12519 },
12520 label_position: Some(label_position.clone()),
12521 source: Default::default(),
12522 });
12523 let (src_delta, scene_delta) = frontend
12524 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12525 .await
12526 .unwrap();
12527 insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12528 assert_eq!(
12529 scene_delta.new_graph.objects.len(),
12530 5,
12531 "{:#?}",
12532 scene_delta.new_graph.objects
12533 );
12534 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12535 let sketch = expect_sketch(sketch_object);
12536 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12537 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12538 panic!("Expected constraint object");
12539 };
12540 let Constraint::VerticalDistance(distance) = constraint else {
12541 panic!("Expected vertical distance constraint");
12542 };
12543 assert_eq!(distance.label_position, Some(label_position));
12544
12545 mock_ctx.close().await;
12546 }
12547
12548 #[tokio::test(flavor = "multi_thread")]
12549 async fn test_add_fixed_standalone_point() {
12550 let initial_source = "\
12551sketch(on = XY) {
12552 point(at = [var 1, var 2])
12553}
12554";
12555
12556 let program = Program::parse(initial_source).unwrap().0.unwrap();
12557
12558 let mut frontend = FrontendState::new();
12559
12560 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12561 let mock_ctx = ExecutorContext::new_mock(None).await;
12562 let version = Version(0);
12563
12564 frontend.hack_set_program(&ctx, program).await.unwrap();
12565 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12566 let sketch_id = sketch_object.id;
12567 let sketch = expect_sketch(sketch_object);
12568 let point_id = *sketch.segments.first().unwrap();
12569
12570 let (src_delta, scene_delta) = frontend
12571 .add_constraint(
12572 &mock_ctx,
12573 version,
12574 sketch_id,
12575 Constraint::Fixed(Fixed {
12576 points: vec![FixedPoint {
12577 point: point_id,
12578 position: Point2d {
12579 x: Number {
12580 value: 2.0,
12581 units: NumericSuffix::Mm,
12582 },
12583 y: Number {
12584 value: 3.0,
12585 units: NumericSuffix::Mm,
12586 },
12587 },
12588 }],
12589 }),
12590 )
12591 .await
12592 .unwrap();
12593 insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12594 assert_eq!(
12595 scene_delta.new_graph.objects.len(),
12596 4,
12597 "{:#?}",
12598 scene_delta.new_graph.objects
12599 );
12600
12601 ctx.close().await;
12602 mock_ctx.close().await;
12603 }
12604
12605 #[tokio::test(flavor = "multi_thread")]
12606 async fn test_add_fixed_multiple_points() {
12607 let initial_source = "\
12608sketch(on = XY) {
12609 point(at = [var 1, var 2])
12610 point(at = [var 3, var 4])
12611}
12612";
12613
12614 let program = Program::parse(initial_source).unwrap().0.unwrap();
12615
12616 let mut frontend = FrontendState::new();
12617
12618 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12619 let mock_ctx = ExecutorContext::new_mock(None).await;
12620 let version = Version(0);
12621
12622 frontend.hack_set_program(&ctx, program).await.unwrap();
12623 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12624 let sketch_id = sketch_object.id;
12625 let sketch = expect_sketch(sketch_object);
12626 let point0_id = *sketch.segments.first().unwrap();
12627 let point1_id = *sketch.segments.get(1).unwrap();
12628
12629 let (src_delta, scene_delta) = frontend
12630 .add_constraint(
12631 &mock_ctx,
12632 version,
12633 sketch_id,
12634 Constraint::Fixed(Fixed {
12635 points: vec![
12636 FixedPoint {
12637 point: point0_id,
12638 position: Point2d {
12639 x: Number {
12640 value: 2.0,
12641 units: NumericSuffix::Mm,
12642 },
12643 y: Number {
12644 value: 3.0,
12645 units: NumericSuffix::Mm,
12646 },
12647 },
12648 },
12649 FixedPoint {
12650 point: point1_id,
12651 position: Point2d {
12652 x: Number {
12653 value: 4.0,
12654 units: NumericSuffix::Mm,
12655 },
12656 y: Number {
12657 value: 5.0,
12658 units: NumericSuffix::Mm,
12659 },
12660 },
12661 },
12662 ],
12663 }),
12664 )
12665 .await
12666 .unwrap();
12667 insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12668 assert_eq!(
12669 scene_delta.new_graph.objects.len(),
12670 6,
12671 "{:#?}",
12672 scene_delta.new_graph.objects
12673 );
12674
12675 ctx.close().await;
12676 mock_ctx.close().await;
12677 }
12678
12679 #[tokio::test(flavor = "multi_thread")]
12680 async fn test_add_fixed_owned_point() {
12681 let initial_source = "\
12682sketch(on = XY) {
12683 line(start = [var 1, var 2], end = [var 3, var 4])
12684}
12685";
12686
12687 let program = Program::parse(initial_source).unwrap().0.unwrap();
12688
12689 let mut frontend = FrontendState::new();
12690
12691 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12692 let mock_ctx = ExecutorContext::new_mock(None).await;
12693 let version = Version(0);
12694
12695 frontend.hack_set_program(&ctx, program).await.unwrap();
12696 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12697 let sketch_id = sketch_object.id;
12698 let sketch = expect_sketch(sketch_object);
12699 let line_start_id = *sketch.segments.first().unwrap();
12700
12701 let (src_delta, scene_delta) = frontend
12702 .add_constraint(
12703 &mock_ctx,
12704 version,
12705 sketch_id,
12706 Constraint::Fixed(Fixed {
12707 points: vec![FixedPoint {
12708 point: line_start_id,
12709 position: Point2d {
12710 x: Number {
12711 value: 2.0,
12712 units: NumericSuffix::Mm,
12713 },
12714 y: Number {
12715 value: 3.0,
12716 units: NumericSuffix::Mm,
12717 },
12718 },
12719 }],
12720 }),
12721 )
12722 .await
12723 .unwrap();
12724 insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12725 assert_eq!(
12726 scene_delta.new_graph.objects.len(),
12727 6,
12728 "{:#?}",
12729 scene_delta.new_graph.objects
12730 );
12731
12732 ctx.close().await;
12733 mock_ctx.close().await;
12734 }
12735
12736 #[tokio::test(flavor = "multi_thread")]
12737 async fn test_radius_error_cases() {
12738 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12739 let mock_ctx = ExecutorContext::new_mock(None).await;
12740 let version = Version(0);
12741
12742 let initial_source_point = "\
12744sketch(on = XY) {
12745 point(at = [var 1, var 2])
12746}
12747";
12748 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12749 let mut frontend_point = FrontendState::new();
12750 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12751 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12752 let sketch_id_point = sketch_object_point.id;
12753 let sketch_point = expect_sketch(sketch_object_point);
12754 let point_id = *sketch_point.segments.first().unwrap();
12755
12756 let constraint_point = Constraint::Radius(Radius {
12757 arc: point_id,
12758 radius: Number {
12759 value: 5.0,
12760 units: NumericSuffix::Mm,
12761 },
12762 label_position: None,
12763 source: Default::default(),
12764 });
12765 let result_point = frontend_point
12766 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12767 .await;
12768 assert!(result_point.is_err(), "Single point should error for radius");
12769
12770 let initial_source_line = "\
12772sketch(on = XY) {
12773 line(start = [var 1, var 2], end = [var 3, var 4])
12774}
12775";
12776 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12777 let mut frontend_line = FrontendState::new();
12778 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12779 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12780 let sketch_id_line = sketch_object_line.id;
12781 let sketch_line = expect_sketch(sketch_object_line);
12782 let line_id = *sketch_line.segments.first().unwrap();
12783
12784 let constraint_line = Constraint::Radius(Radius {
12785 arc: line_id,
12786 radius: Number {
12787 value: 5.0,
12788 units: NumericSuffix::Mm,
12789 },
12790 label_position: None,
12791 source: Default::default(),
12792 });
12793 let result_line = frontend_line
12794 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12795 .await;
12796 assert!(result_line.is_err(), "Single line segment should error for radius");
12797
12798 ctx.close().await;
12799 mock_ctx.close().await;
12800 }
12801
12802 #[tokio::test(flavor = "multi_thread")]
12803 async fn test_diameter_single_arc_segment() {
12804 let initial_source = "\
12805sketch(on = XY) {
12806 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12807}
12808";
12809
12810 let program = Program::parse(initial_source).unwrap().0.unwrap();
12811
12812 let mut frontend = FrontendState::new();
12813
12814 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12815 let mock_ctx = ExecutorContext::new_mock(None).await;
12816 let version = Version(0);
12817
12818 frontend.hack_set_program(&ctx, program).await.unwrap();
12819 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12820 let sketch_id = sketch_object.id;
12821 let sketch = expect_sketch(sketch_object);
12822 let arc_id = sketch
12824 .segments
12825 .iter()
12826 .find(|&seg_id| {
12827 let obj = frontend.scene_graph.objects.get(seg_id.0);
12828 matches!(
12829 obj.map(|o| &o.kind),
12830 Some(ObjectKind::Segment {
12831 segment: Segment::Arc(_)
12832 })
12833 )
12834 })
12835 .unwrap();
12836
12837 let constraint = Constraint::Diameter(Diameter {
12838 arc: *arc_id,
12839 diameter: Number {
12840 value: 10.0,
12841 units: NumericSuffix::Mm,
12842 },
12843 label_position: None,
12844 source: Default::default(),
12845 });
12846 let (src_delta, scene_delta) = frontend
12847 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12848 .await
12849 .unwrap();
12850 insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12851 assert_eq!(
12852 scene_delta.new_graph.objects.len(),
12853 7, "{:#?}",
12855 scene_delta.new_graph.objects
12856 );
12857
12858 ctx.close().await;
12859 mock_ctx.close().await;
12860 }
12861
12862 #[tokio::test(flavor = "multi_thread")]
12863 async fn test_diameter_single_arc_segment_with_label_position() {
12864 let initial_source = "\
12865sketch(on = XY) {
12866 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12867}
12868";
12869
12870 let program = Program::parse(initial_source).unwrap().0.unwrap();
12871 let mut frontend = FrontendState::new();
12872 let mock_ctx = ExecutorContext::new_mock(None).await;
12873 let version = Version(0);
12874
12875 frontend.program = program.clone();
12876 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12877 frontend.update_state_after_exec(outcome, true);
12878 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12879 let sketch_id = sketch_object.id;
12880 let sketch = expect_sketch(sketch_object);
12881 let arc_id = sketch
12882 .segments
12883 .iter()
12884 .find(|&seg_id| {
12885 let obj = frontend.scene_graph.objects.get(seg_id.0);
12886 matches!(
12887 obj.map(|o| &o.kind),
12888 Some(ObjectKind::Segment {
12889 segment: Segment::Arc(_)
12890 })
12891 )
12892 })
12893 .unwrap();
12894
12895 let label_position = Point2d {
12896 x: Number {
12897 value: 10.0,
12898 units: NumericSuffix::Mm,
12899 },
12900 y: Number {
12901 value: 11.0,
12902 units: NumericSuffix::Mm,
12903 },
12904 };
12905 let constraint = Constraint::Diameter(Diameter {
12906 arc: *arc_id,
12907 diameter: Number {
12908 value: 10.0,
12909 units: NumericSuffix::Mm,
12910 },
12911 label_position: Some(label_position.clone()),
12912 source: Default::default(),
12913 });
12914 let (src_delta, scene_delta) = frontend
12915 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12916 .await
12917 .unwrap();
12918 insta::assert_snapshot!(
12919 "test_diameter_single_arc_segment_with_label_position",
12920 src_delta.text.as_str()
12921 );
12922
12923 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12924 let sketch = expect_sketch(sketch_object);
12925 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12926 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12927 panic!("Expected constraint object");
12928 };
12929 let Constraint::Diameter(diameter) = constraint else {
12930 panic!("Expected diameter constraint");
12931 };
12932 assert_eq!(diameter.label_position, Some(label_position));
12933
12934 mock_ctx.close().await;
12935 }
12936
12937 #[tokio::test(flavor = "multi_thread")]
12938 async fn test_edit_diameter_constraint_label_position() {
12939 let initial_source = "\
12940sketch(on = XY) {
12941 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12942 diameter(arc1) == 10mm
12943}
12944";
12945
12946 let program = Program::parse(initial_source).unwrap().0.unwrap();
12947 let mut frontend = FrontendState::new();
12948 let mock_ctx = ExecutorContext::new_mock(None).await;
12949 let version = Version(0);
12950
12951 frontend.program = program.clone();
12952 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12953 frontend.update_state_after_exec(outcome, true);
12954 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12955 let sketch_id = sketch_object.id;
12956 let sketch = expect_sketch(sketch_object);
12957 let constraint_id = sketch.constraints[0];
12958 let label_position = Point2d {
12959 x: Number {
12960 value: 10.0,
12961 units: NumericSuffix::Mm,
12962 },
12963 y: Number {
12964 value: 11.0,
12965 units: NumericSuffix::Mm,
12966 },
12967 };
12968
12969 let (src_delta, scene_delta) = frontend
12970 .edit_distance_constraint_label_position(
12971 &mock_ctx,
12972 version,
12973 sketch_id,
12974 constraint_id,
12975 label_position.clone(),
12976 vec![],
12977 )
12978 .await
12979 .unwrap();
12980 insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12981
12982 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12983 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12984 panic!("Expected constraint object");
12985 };
12986 let Constraint::Diameter(diameter) = constraint else {
12987 panic!("Expected diameter constraint");
12988 };
12989 assert_eq!(diameter.label_position, Some(label_position));
12990
12991 mock_ctx.close().await;
12992 }
12993
12994 #[tokio::test(flavor = "multi_thread")]
12995 async fn test_diameter_error_cases() {
12996 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12997 let mock_ctx = ExecutorContext::new_mock(None).await;
12998 let version = Version(0);
12999
13000 let initial_source_point = "\
13002sketch(on = XY) {
13003 point(at = [var 1, var 2])
13004}
13005";
13006 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
13007 let mut frontend_point = FrontendState::new();
13008 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
13009 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
13010 let sketch_id_point = sketch_object_point.id;
13011 let sketch_point = expect_sketch(sketch_object_point);
13012 let point_id = *sketch_point.segments.first().unwrap();
13013
13014 let constraint_point = Constraint::Diameter(Diameter {
13015 arc: point_id,
13016 diameter: Number {
13017 value: 10.0,
13018 units: NumericSuffix::Mm,
13019 },
13020 label_position: None,
13021 source: Default::default(),
13022 });
13023 let result_point = frontend_point
13024 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
13025 .await;
13026 assert!(result_point.is_err(), "Single point should error for diameter");
13027
13028 let initial_source_line = "\
13030sketch(on = XY) {
13031 line(start = [var 1, var 2], end = [var 3, var 4])
13032}
13033";
13034 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
13035 let mut frontend_line = FrontendState::new();
13036 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
13037 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
13038 let sketch_id_line = sketch_object_line.id;
13039 let sketch_line = expect_sketch(sketch_object_line);
13040 let line_id = *sketch_line.segments.first().unwrap();
13041
13042 let constraint_line = Constraint::Diameter(Diameter {
13043 arc: line_id,
13044 diameter: Number {
13045 value: 10.0,
13046 units: NumericSuffix::Mm,
13047 },
13048 label_position: None,
13049 source: Default::default(),
13050 });
13051 let result_line = frontend_line
13052 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
13053 .await;
13054 assert!(result_line.is_err(), "Single line segment should error for diameter");
13055
13056 ctx.close().await;
13057 mock_ctx.close().await;
13058 }
13059
13060 #[tokio::test(flavor = "multi_thread")]
13061 async fn test_line_horizontal() {
13062 let initial_source = "\
13063sketch(on = XY) {
13064 line(start = [var 1, var 2], end = [var 3, var 4])
13065}
13066";
13067
13068 let program = Program::parse(initial_source).unwrap().0.unwrap();
13069
13070 let mut frontend = FrontendState::new();
13071
13072 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13073 let mock_ctx = ExecutorContext::new_mock(None).await;
13074 let version = Version(0);
13075
13076 frontend.hack_set_program(&ctx, program).await.unwrap();
13077 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13078 let sketch_id = sketch_object.id;
13079 let sketch = expect_sketch(sketch_object);
13080 let line1_id = *sketch.segments.get(2).unwrap();
13081
13082 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13083 let (src_delta, scene_delta) = frontend
13084 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13085 .await
13086 .unwrap();
13087 insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13088 assert_eq!(
13089 scene_delta.new_graph.objects.len(),
13090 6,
13091 "{:#?}",
13092 scene_delta.new_graph.objects
13093 );
13094
13095 ctx.close().await;
13096 mock_ctx.close().await;
13097 }
13098
13099 #[tokio::test(flavor = "multi_thread")]
13100 async fn test_control_point_spline_edge_horizontal() {
13101 let initial_source = "\
13102@settings(experimentalFeatures = allow)
13103splineSketch = sketch(on = XY) {
13104 controlPointSpline1 = controlPointSpline(points = [
13105 [var 0mm, var 0mm],
13106 [var 10mm, var 20mm],
13107 [var 20mm, var 0mm],
13108 ])
13109}
13110";
13111
13112 let program = Program::parse(initial_source).unwrap().0.unwrap();
13113
13114 let mut frontend = FrontendState::new();
13115
13116 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13117 let mock_ctx = ExecutorContext::new_mock(None).await;
13118 let version = Version(0);
13119
13120 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13121 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13122 let sketch_id = sketch_object.id;
13123 let sketch = expect_sketch(sketch_object);
13124 let spline_id = sketch
13125 .segments
13126 .iter()
13127 .copied()
13128 .find(|seg_id| {
13129 matches!(
13130 &frontend.scene_graph.objects[seg_id.0].kind,
13131 ObjectKind::Segment {
13132 segment: Segment::ControlPointSpline(_)
13133 }
13134 )
13135 })
13136 .expect("Expected a control point spline segment in sketch");
13137 let edge_id = frontend
13138 .scene_graph
13139 .objects
13140 .iter()
13141 .find_map(|obj| match &obj.kind {
13142 ObjectKind::Segment {
13143 segment: Segment::Line(line),
13144 } if line.owner == Some(spline_id) => Some(obj.id),
13145 _ => None,
13146 })
13147 .expect("Expected an owned control-polygon edge");
13148
13149 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13150 let (src_delta, _) = frontend
13151 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13152 .await
13153 .unwrap();
13154 assert!(
13155 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13156 "Expected horizontal constraint on spline edge, got: {}",
13157 src_delta.text
13158 );
13159
13160 ctx.close().await;
13161 mock_ctx.close().await;
13162 }
13163
13164 #[tokio::test(flavor = "multi_thread")]
13165 async fn test_control_point_spline_edge_angle() {
13166 let initial_source = "\
13167@settings(experimentalFeatures = allow)
13168splineSketch = sketch(on = XY) {
13169 controlPointSpline1 = controlPointSpline(points = [
13170 [var 0mm, var 0mm],
13171 [var 10mm, var 20mm],
13172 [var 20mm, var 0mm],
13173 ])
13174
13175 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13176}
13177";
13178
13179 let program = Program::parse(initial_source).unwrap().0.unwrap();
13180
13181 let mut frontend = FrontendState::new();
13182
13183 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13184 let mock_ctx = ExecutorContext::new_mock(None).await;
13185 let version = Version(0);
13186
13187 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13188 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13189 let sketch_id = sketch_object.id;
13190 let sketch = expect_sketch(sketch_object);
13191 let spline_id = sketch
13192 .segments
13193 .iter()
13194 .copied()
13195 .find(|seg_id| {
13196 matches!(
13197 &frontend.scene_graph.objects[seg_id.0].kind,
13198 ObjectKind::Segment {
13199 segment: Segment::ControlPointSpline(_)
13200 }
13201 )
13202 })
13203 .expect("Expected a control point spline segment in sketch");
13204 let edge_id = frontend
13205 .scene_graph
13206 .objects
13207 .iter()
13208 .find_map(|obj| match &obj.kind {
13209 ObjectKind::Segment {
13210 segment: Segment::Line(line),
13211 } if line.owner == Some(spline_id) => Some(obj.id),
13212 _ => None,
13213 })
13214 .expect("Expected an owned control-polygon edge");
13215 let line1_id = frontend
13216 .scene_graph
13217 .objects
13218 .iter()
13219 .find_map(|obj| match &obj.kind {
13220 ObjectKind::Segment {
13221 segment: Segment::Line(line),
13222 } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
13223 _ => None,
13224 })
13225 .or_else(|| {
13226 sketch.segments.iter().copied().find(|seg_id| {
13227 matches!(
13228 &frontend.scene_graph.objects[seg_id.0].kind,
13229 ObjectKind::Segment {
13230 segment: Segment::Line(line),
13231 } if line.owner.is_none()
13232 )
13233 })
13234 })
13235 .expect("Expected a standalone line segment in sketch");
13236
13237 let constraint = Constraint::Angle(Angle {
13238 lines: vec![line1_id, edge_id],
13239 angle: Number {
13240 value: 30.0,
13241 units: NumericSuffix::Deg,
13242 },
13243 sector: None,
13244 inverse: None,
13245 label_position: None,
13246 source: Default::default(),
13247 });
13248 let (src_delta, _) = frontend
13249 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13250 .await
13251 .unwrap();
13252 assert!(
13253 src_delta
13254 .text
13255 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13256 "Expected angle constraint on spline edge, got: {}",
13257 src_delta.text
13258 );
13259
13260 ctx.close().await;
13261 mock_ctx.close().await;
13262 }
13263
13264 #[tokio::test(flavor = "multi_thread")]
13265 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13266 let initial_source = "\
13267@settings(experimentalFeatures = allow)
13268splineSketch = sketch(on = XY) {
13269 spline1 = controlPointSpline(points = [
13270 [var 0mm, var 0mm],
13271 [var 10mm, var 20mm],
13272 [var 20mm, var 0mm],
13273 ])
13274 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13275 coincident([spline1.controls[1], spline1.edges[0]])
13276 coincident([spline1.controls[0], line1])
13277}
13278";
13279
13280 let program = Program::parse(initial_source).unwrap().0.unwrap();
13281
13282 let mut frontend = FrontendState::new();
13283
13284 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13285 let mock_ctx = ExecutorContext::new_mock(None).await;
13286
13287 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13288
13289 let ui_scene_graph = frontend.scene_graph_for_ui();
13290 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13291 let sketch = expect_sketch(sketch_object);
13292
13293 assert_eq!(
13294 sketch.constraints.len(),
13295 1,
13296 "Expected only the external coincident constraint to remain visible in the UI scene graph"
13297 );
13298
13299 let visible_constraints = ui_scene_graph
13300 .objects
13301 .iter()
13302 .filter_map(|object| match &object.kind {
13303 ObjectKind::Constraint {
13304 constraint: Constraint::Coincident(coincident),
13305 } => Some(coincident.clone()),
13306 _ => None,
13307 })
13308 .collect::<Vec<_>>();
13309
13310 assert_eq!(
13311 visible_constraints.len(),
13312 1,
13313 "Expected only one coincident constraint object in the UI scene graph"
13314 );
13315 assert_eq!(
13316 visible_constraints[0].get_segments().len(),
13317 2,
13318 "Expected the remaining visible coincident constraint to reference two segments"
13319 );
13320
13321 ctx.close().await;
13322 mock_ctx.close().await;
13323 }
13324
13325 #[tokio::test(flavor = "multi_thread")]
13326 async fn test_edit_control_point_spline_can_append_control_point() {
13327 let initial_source = "\
13328@settings(experimentalFeatures = allow)
13329splineSketch = sketch(on = XY) {
13330 controlPointSpline(points = [
13331 [var 0mm, var 0mm],
13332 [var 10mm, var 20mm],
13333 [var 20mm, var 0mm],
13334 ])
13335}
13336";
13337
13338 let program = Program::parse(initial_source).unwrap().0.unwrap();
13339
13340 let mut frontend = FrontendState::new();
13341
13342 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13343 let mock_ctx = ExecutorContext::new_mock(None).await;
13344 let version = Version(0);
13345
13346 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13347 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13348 let sketch_id = sketch_object.id;
13349 let sketch = expect_sketch(sketch_object);
13350 let spline_id = sketch
13351 .segments
13352 .iter()
13353 .copied()
13354 .find(|seg_id| {
13355 matches!(
13356 &frontend.scene_graph.objects[seg_id.0].kind,
13357 ObjectKind::Segment {
13358 segment: Segment::ControlPointSpline(_)
13359 }
13360 )
13361 })
13362 .expect("Expected a control point spline segment in sketch");
13363
13364 let ctor = ControlPointSplineCtor {
13365 points: vec![
13366 Point2d {
13367 x: Expr::Var(Number {
13368 value: 0.0,
13369 units: NumericSuffix::Mm,
13370 }),
13371 y: Expr::Var(Number {
13372 value: 0.0,
13373 units: NumericSuffix::Mm,
13374 }),
13375 },
13376 Point2d {
13377 x: Expr::Var(Number {
13378 value: 10.0,
13379 units: NumericSuffix::Mm,
13380 }),
13381 y: Expr::Var(Number {
13382 value: 20.0,
13383 units: NumericSuffix::Mm,
13384 }),
13385 },
13386 Point2d {
13387 x: Expr::Var(Number {
13388 value: 20.0,
13389 units: NumericSuffix::Mm,
13390 }),
13391 y: Expr::Var(Number {
13392 value: 0.0,
13393 units: NumericSuffix::Mm,
13394 }),
13395 },
13396 Point2d {
13397 x: Expr::Var(Number {
13398 value: 30.0,
13399 units: NumericSuffix::Mm,
13400 }),
13401 y: Expr::Var(Number {
13402 value: 10.0,
13403 units: NumericSuffix::Mm,
13404 }),
13405 },
13406 ],
13407 construction: None,
13408 };
13409
13410 let segments = vec![ExistingSegmentCtor {
13411 id: spline_id,
13412 ctor: SegmentCtor::ControlPointSpline(ctor),
13413 }];
13414 let (src_delta, scene_delta) = frontend
13415 .edit_segments(&mock_ctx, version, sketch_id, segments)
13416 .await
13417 .unwrap();
13418
13419 assert!(
13420 src_delta.text.contains("[var 30mm, var 10mm]"),
13421 "Expected appended spline control point in source, got: {}",
13422 src_delta.text
13423 );
13424
13425 assert!(
13426 scene_delta.invalidates_ids,
13427 "Expected appending a spline control point to invalidate ids"
13428 );
13429 let updated_spline = scene_delta
13430 .new_graph
13431 .objects
13432 .iter()
13433 .find_map(|obj| match &obj.kind {
13434 ObjectKind::Segment {
13435 segment: Segment::ControlPointSpline(updated_spline),
13436 } if updated_spline.controls.len() == 4 => Some(updated_spline),
13437 _ => None,
13438 })
13439 .expect("Expected edited scene graph to contain a four-point control point spline");
13440 assert_eq!(
13441 updated_spline.controls.len(),
13442 4,
13443 "Expected edited spline to expose four control points"
13444 );
13445
13446 ctx.close().await;
13447 mock_ctx.close().await;
13448 }
13449
13450 #[tokio::test(flavor = "multi_thread")]
13451 async fn test_line_vertical() {
13452 let initial_source = "\
13453sketch(on = XY) {
13454 line(start = [var 1, var 2], end = [var 3, var 4])
13455}
13456";
13457
13458 let program = Program::parse(initial_source).unwrap().0.unwrap();
13459
13460 let mut frontend = FrontendState::new();
13461
13462 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13463 let mock_ctx = ExecutorContext::new_mock(None).await;
13464 let version = Version(0);
13465
13466 frontend.hack_set_program(&ctx, program).await.unwrap();
13467 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13468 let sketch_id = sketch_object.id;
13469 let sketch = expect_sketch(sketch_object);
13470 let line1_id = *sketch.segments.get(2).unwrap();
13471
13472 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13473 let (src_delta, scene_delta) = frontend
13474 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13475 .await
13476 .unwrap();
13477 insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13478 assert_eq!(
13479 scene_delta.new_graph.objects.len(),
13480 6,
13481 "{:#?}",
13482 scene_delta.new_graph.objects
13483 );
13484
13485 ctx.close().await;
13486 mock_ctx.close().await;
13487 }
13488
13489 #[tokio::test(flavor = "multi_thread")]
13490 async fn test_points_vertical() {
13491 let initial_source = "\
13492sketch001 = sketch(on = XY) {
13493 p0 = point(at = [var -2.23mm, var 3.1mm])
13494 pf = point(at = [4, 4])
13495}
13496";
13497
13498 let program = Program::parse(initial_source).unwrap().0.unwrap();
13499
13500 let mut frontend = FrontendState::new();
13501
13502 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13503 let mock_ctx = ExecutorContext::new_mock(None).await;
13504 let version = Version(0);
13505
13506 frontend.hack_set_program(&ctx, program).await.unwrap();
13507 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13508 let sketch_id = sketch_object.id;
13509 let sketch = expect_sketch(sketch_object);
13510 let point_ids = vec![
13511 sketch.segments.first().unwrap().to_owned(),
13512 sketch.segments.get(1).unwrap().to_owned(),
13513 ];
13514
13515 let constraint = Constraint::Vertical(Vertical::Points {
13516 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13517 });
13518 let (src_delta, scene_delta) = frontend
13519 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13520 .await
13521 .unwrap();
13522 insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13523 assert_eq!(
13524 scene_delta.new_graph.objects.len(),
13525 5,
13526 "{:#?}",
13527 scene_delta.new_graph.objects
13528 );
13529
13530 ctx.close().await;
13531 mock_ctx.close().await;
13532 }
13533
13534 #[tokio::test(flavor = "multi_thread")]
13535 async fn test_points_horizontal() {
13536 let initial_source = "\
13537sketch001 = sketch(on = XY) {
13538 p0 = point(at = [var -2.23mm, var 3.1mm])
13539 pf = point(at = [4, 4])
13540}
13541";
13542
13543 let program = Program::parse(initial_source).unwrap().0.unwrap();
13544
13545 let mut frontend = FrontendState::new();
13546
13547 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13548 let mock_ctx = ExecutorContext::new_mock(None).await;
13549 let version = Version(0);
13550
13551 frontend.hack_set_program(&ctx, program).await.unwrap();
13552 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13553 let sketch_id = sketch_object.id;
13554 let sketch = expect_sketch(sketch_object);
13555 let point_ids = vec![
13556 sketch.segments.first().unwrap().to_owned(),
13557 sketch.segments.get(1).unwrap().to_owned(),
13558 ];
13559
13560 let constraint = Constraint::Horizontal(Horizontal::Points {
13561 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13562 });
13563 let (src_delta, scene_delta) = frontend
13564 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13565 .await
13566 .unwrap();
13567 insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13568 assert_eq!(
13569 scene_delta.new_graph.objects.len(),
13570 5,
13571 "{:#?}",
13572 scene_delta.new_graph.objects
13573 );
13574
13575 ctx.close().await;
13576 mock_ctx.close().await;
13577 }
13578
13579 #[tokio::test(flavor = "multi_thread")]
13580 async fn test_point_horizontal_with_origin() {
13581 let initial_source = "\
13582sketch001 = sketch(on = XY) {
13583 p0 = point(at = [var -2.23mm, var 3.1mm])
13584}
13585";
13586
13587 let program = Program::parse(initial_source).unwrap().0.unwrap();
13588
13589 let mut frontend = FrontendState::new();
13590
13591 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13592 let mock_ctx = ExecutorContext::new_mock(None).await;
13593 let version = Version(0);
13594
13595 frontend.hack_set_program(&ctx, program).await.unwrap();
13596 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13597 let sketch_id = sketch_object.id;
13598 let sketch = expect_sketch(sketch_object);
13599 let point_id = *sketch.segments.first().unwrap();
13600
13601 let constraint = Constraint::Horizontal(Horizontal::Points {
13602 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13603 });
13604 let (src_delta, scene_delta) = frontend
13605 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13606 .await
13607 .unwrap();
13608 insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13609 assert_eq!(
13610 scene_delta.new_graph.objects.len(),
13611 4,
13612 "{:#?}",
13613 scene_delta.new_graph.objects
13614 );
13615
13616 ctx.close().await;
13617 mock_ctx.close().await;
13618 }
13619
13620 #[tokio::test(flavor = "multi_thread")]
13621 async fn test_lines_equal_length() {
13622 let initial_source = "\
13623sketch(on = XY) {
13624 line(start = [var 1, var 2], end = [var 3, var 4])
13625 line(start = [var 5, var 6], end = [var 7, var 8])
13626}
13627";
13628
13629 let program = Program::parse(initial_source).unwrap().0.unwrap();
13630
13631 let mut frontend = FrontendState::new();
13632
13633 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13634 let mock_ctx = ExecutorContext::new_mock(None).await;
13635 let version = Version(0);
13636
13637 frontend.hack_set_program(&ctx, program).await.unwrap();
13638 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13639 let sketch_id = sketch_object.id;
13640 let sketch = expect_sketch(sketch_object);
13641 let line1_id = *sketch.segments.get(2).unwrap();
13642 let line2_id = *sketch.segments.get(5).unwrap();
13643
13644 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13645 lines: vec![line1_id, line2_id],
13646 });
13647 let (src_delta, scene_delta) = frontend
13648 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13649 .await
13650 .unwrap();
13651 insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13652 assert_eq!(
13653 scene_delta.new_graph.objects.len(),
13654 9,
13655 "{:#?}",
13656 scene_delta.new_graph.objects
13657 );
13658
13659 ctx.close().await;
13660 mock_ctx.close().await;
13661 }
13662
13663 #[tokio::test(flavor = "multi_thread")]
13664 async fn test_add_constraint_multi_line_equal_length() {
13665 let initial_source = "\
13666sketch(on = XY) {
13667 line(start = [var 1, var 2], end = [var 3, var 4])
13668 line(start = [var 5, var 6], end = [var 7, var 8])
13669 line(start = [var 9, var 10], end = [var 11, var 12])
13670}
13671";
13672
13673 let program = Program::parse(initial_source).unwrap().0.unwrap();
13674
13675 let mut frontend = FrontendState::new();
13676 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13677 let mock_ctx = ExecutorContext::new_mock(None).await;
13678 let version = Version(0);
13679
13680 frontend.hack_set_program(&ctx, program).await.unwrap();
13681 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13682 let sketch_id = sketch_object.id;
13683 let sketch = expect_sketch(sketch_object);
13684 let line1_id = *sketch.segments.get(2).unwrap();
13685 let line2_id = *sketch.segments.get(5).unwrap();
13686 let line3_id = *sketch.segments.get(8).unwrap();
13687
13688 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13689 lines: vec![line1_id, line2_id, line3_id],
13690 });
13691 let (src_delta, scene_delta) = frontend
13692 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13693 .await
13694 .unwrap();
13695 insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13696 let constraints = scene_delta
13697 .new_graph
13698 .objects
13699 .iter()
13700 .filter_map(|obj| {
13701 let ObjectKind::Constraint { constraint } = &obj.kind else {
13702 return None;
13703 };
13704 Some(constraint)
13705 })
13706 .collect::<Vec<_>>();
13707
13708 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13709 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13710 panic!("expected equal length constraint, got {:?}", constraints[0]);
13711 };
13712 assert_eq!(lines_equal_length.lines.len(), 3);
13713
13714 ctx.close().await;
13715 mock_ctx.close().await;
13716 }
13717
13718 #[tokio::test(flavor = "multi_thread")]
13719 async fn test_lines_parallel() {
13720 let initial_source = "\
13721sketch(on = XY) {
13722 line(start = [var 1, var 2], end = [var 3, var 4])
13723 line(start = [var 5, var 6], end = [var 7, var 8])
13724}
13725";
13726
13727 let program = Program::parse(initial_source).unwrap().0.unwrap();
13728
13729 let mut frontend = FrontendState::new();
13730
13731 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13732 let mock_ctx = ExecutorContext::new_mock(None).await;
13733 let version = Version(0);
13734
13735 frontend.hack_set_program(&ctx, program).await.unwrap();
13736 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13737 let sketch_id = sketch_object.id;
13738 let sketch = expect_sketch(sketch_object);
13739 let line1_id = *sketch.segments.get(2).unwrap();
13740 let line2_id = *sketch.segments.get(5).unwrap();
13741
13742 let constraint = Constraint::Parallel(Parallel {
13743 lines: vec![line1_id, line2_id],
13744 });
13745 let (src_delta, scene_delta) = frontend
13746 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13747 .await
13748 .unwrap();
13749 insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13750 assert_eq!(
13751 scene_delta.new_graph.objects.len(),
13752 9,
13753 "{:#?}",
13754 scene_delta.new_graph.objects
13755 );
13756
13757 ctx.close().await;
13758 mock_ctx.close().await;
13759 }
13760
13761 #[tokio::test(flavor = "multi_thread")]
13762 async fn test_lines_parallel_multiline() {
13763 let initial_source = "\
13764sketch(on = XY) {
13765 line(start = [var 1, var 2], end = [var 3, var 4])
13766 line(start = [var 5, var 6], end = [var 7, var 8])
13767 line(start = [var 9, var 10], end = [var 11, var 12])
13768}
13769";
13770
13771 let program = Program::parse(initial_source).unwrap().0.unwrap();
13772
13773 let mut frontend = FrontendState::new();
13774
13775 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13776 let mock_ctx = ExecutorContext::new_mock(None).await;
13777 let version = Version(0);
13778
13779 frontend.hack_set_program(&ctx, program).await.unwrap();
13780 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13781 let sketch_id = sketch_object.id;
13782 let sketch = expect_sketch(sketch_object);
13783 let line1_id = *sketch.segments.get(2).unwrap();
13784 let line2_id = *sketch.segments.get(5).unwrap();
13785 let line3_id = *sketch.segments.get(8).unwrap();
13786
13787 let constraint = Constraint::Parallel(Parallel {
13788 lines: vec![line1_id, line2_id, line3_id],
13789 });
13790 let (src_delta, scene_delta) = frontend
13791 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13792 .await
13793 .unwrap();
13794 insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13795
13796 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13797 let sketch = expect_sketch(sketch_object);
13798 assert_eq!(sketch.constraints.len(), 1);
13799
13800 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13801 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13802 panic!("Expected constraint object");
13803 };
13804 let Constraint::Parallel(parallel) = constraint else {
13805 panic!("Expected parallel constraint");
13806 };
13807 assert_eq!(parallel.lines.len(), 3);
13808
13809 ctx.close().await;
13810 mock_ctx.close().await;
13811 }
13812
13813 #[tokio::test(flavor = "multi_thread")]
13814 async fn test_lines_perpendicular() {
13815 let initial_source = "\
13816sketch(on = XY) {
13817 line(start = [var 1, var 2], end = [var 3, var 4])
13818 line(start = [var 5, var 6], end = [var 7, var 8])
13819}
13820";
13821
13822 let program = Program::parse(initial_source).unwrap().0.unwrap();
13823
13824 let mut frontend = FrontendState::new();
13825
13826 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13827 let mock_ctx = ExecutorContext::new_mock(None).await;
13828 let version = Version(0);
13829
13830 frontend.hack_set_program(&ctx, program).await.unwrap();
13831 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13832 let sketch_id = sketch_object.id;
13833 let sketch = expect_sketch(sketch_object);
13834 let line1_id = *sketch.segments.get(2).unwrap();
13835 let line2_id = *sketch.segments.get(5).unwrap();
13836
13837 let constraint = Constraint::Perpendicular(Perpendicular {
13838 lines: vec![line1_id, line2_id],
13839 });
13840 let (src_delta, scene_delta) = frontend
13841 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13842 .await
13843 .unwrap();
13844 insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13845 assert_eq!(
13846 scene_delta.new_graph.objects.len(),
13847 9,
13848 "{:#?}",
13849 scene_delta.new_graph.objects
13850 );
13851
13852 ctx.close().await;
13853 mock_ctx.close().await;
13854 }
13855
13856 #[tokio::test(flavor = "multi_thread")]
13857 async fn test_lines_angle() {
13858 let initial_source = "\
13859sketch(on = XY) {
13860 line(start = [var 1, var 2], end = [var 3, var 4])
13861 line(start = [var 5, var 6], end = [var 7, var 8])
13862}
13863";
13864
13865 let program = Program::parse(initial_source).unwrap().0.unwrap();
13866
13867 let mut frontend = FrontendState::new();
13868
13869 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13870 let mock_ctx = ExecutorContext::new_mock(None).await;
13871 let version = Version(0);
13872
13873 frontend.hack_set_program(&ctx, program).await.unwrap();
13874 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13875 let sketch_id = sketch_object.id;
13876 let sketch = expect_sketch(sketch_object);
13877 let line1_id = *sketch.segments.get(2).unwrap();
13878 let line2_id = *sketch.segments.get(5).unwrap();
13879
13880 let constraint = Constraint::Angle(Angle {
13881 lines: vec![line1_id, line2_id],
13882 angle: Number {
13883 value: 30.0,
13884 units: NumericSuffix::Deg,
13885 },
13886 sector: None,
13887 inverse: None,
13888 label_position: None,
13889 source: Default::default(),
13890 });
13891 let (src_delta, scene_delta) = frontend
13892 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13893 .await
13894 .unwrap();
13895 insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13896 assert_eq!(
13897 scene_delta.new_graph.objects.len(),
13898 9,
13899 "{:#?}",
13900 scene_delta.new_graph.objects
13901 );
13902
13903 ctx.close().await;
13904 mock_ctx.close().await;
13905 }
13906
13907 #[tokio::test(flavor = "multi_thread")]
13908 async fn test_lines_angle_with_sector_uses_angle_dimension() {
13909 let initial_source = "\
13910sketch(on = XY) {
13911 line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13912 line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13913}
13914";
13915
13916 let program = Program::parse(initial_source).unwrap().0.unwrap();
13917
13918 let mut frontend = FrontendState::new();
13919
13920 let mock_ctx = ExecutorContext::new_mock(None).await;
13921 let version = Version(0);
13922
13923 frontend.program = program.clone();
13924 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13925 frontend.update_state_after_exec(outcome, true);
13926 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13927 let sketch_id = sketch_object.id;
13928 let sketch = expect_sketch(sketch_object);
13929 let line1_id = *sketch.segments.get(2).unwrap();
13930 let line2_id = *sketch.segments.get(5).unwrap();
13931
13932 let constraint = Constraint::Angle(Angle {
13933 lines: vec![line1_id, line2_id],
13934 angle: Number {
13935 value: 270.0,
13936 units: NumericSuffix::Deg,
13937 },
13938 sector: Some(1),
13939 inverse: Some(true),
13940 label_position: Some(Point2d {
13941 x: Number {
13942 value: -0.73,
13943 units: NumericSuffix::Mm,
13944 },
13945 y: Number {
13946 value: 0.75,
13947 units: NumericSuffix::Mm,
13948 },
13949 }),
13950 source: Default::default(),
13951 });
13952 let (src_delta, _) = frontend
13953 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13954 .await
13955 .unwrap();
13956 assert_eq!(
13957 src_delta.text.as_str(),
13958 "\
13959sketch(on = XY) {
13960 line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13961 line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13962 angleDimension(
13963 lines = [line1, line2],
13964 sector = 1,
13965 inverse = true,
13966 labelPosition = [-0.73mm, 0.75mm],
13967) == 270deg
13968}
13969"
13970 );
13971
13972 mock_ctx.close().await;
13973 }
13974
13975 #[tokio::test(flavor = "multi_thread")]
13976 async fn test_segments_tangent() {
13977 let initial_source = "\
13978sketch(on = XY) {
13979 line(start = [var 1, var 2], end = [var 3, var 4])
13980 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13981}
13982";
13983
13984 let program = Program::parse(initial_source).unwrap().0.unwrap();
13985
13986 let mut frontend = FrontendState::new();
13987
13988 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13989 let mock_ctx = ExecutorContext::new_mock(None).await;
13990 let version = Version(0);
13991
13992 frontend.hack_set_program(&ctx, program).await.unwrap();
13993 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13994 let sketch_id = sketch_object.id;
13995 let sketch = expect_sketch(sketch_object);
13996 let line1_id = *sketch.segments.get(2).unwrap();
13997 let arc1_id = *sketch.segments.get(6).unwrap();
13998
13999 let constraint = Constraint::Tangent(Tangent {
14000 input: vec![line1_id, arc1_id],
14001 });
14002 let (src_delta, scene_delta) = frontend
14003 .add_constraint(&mock_ctx, version, sketch_id, constraint)
14004 .await
14005 .unwrap();
14006 insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
14007 assert_eq!(
14008 scene_delta.new_graph.objects.len(),
14009 10,
14010 "{:#?}",
14011 scene_delta.new_graph.objects
14012 );
14013
14014 ctx.close().await;
14015 mock_ctx.close().await;
14016 }
14017
14018 #[tokio::test(flavor = "multi_thread")]
14019 async fn test_point_midpoint() {
14020 let initial_source = "\
14021sketch(on = XY) {
14022 point(at = [var 1, var 1])
14023 line(start = [var 0, var 0], end = [var 6, var 4])
14024}
14025";
14026
14027 let program = Program::parse(initial_source).unwrap().0.unwrap();
14028
14029 let mut frontend = FrontendState::new();
14030
14031 let ctx = ExecutorContext::new_mock(None).await;
14032 let version = Version(0);
14033
14034 frontend.program = program.clone();
14035 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14036 frontend.update_state_after_exec(outcome, true);
14037 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14038 let sketch_id = sketch_object.id;
14039 let sketch = expect_sketch(sketch_object);
14040 let point_id = *sketch.segments.first().unwrap();
14041 let line_id = *sketch.segments.get(3).unwrap();
14042
14043 let constraint = Constraint::Midpoint(Midpoint {
14044 point: ConstraintSegment::from(point_id),
14045 segment: line_id,
14046 });
14047 let (src_delta, scene_delta) = frontend
14048 .add_constraint(&ctx, version, sketch_id, constraint)
14049 .await
14050 .unwrap();
14051 insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
14052 assert_eq!(
14053 scene_delta.new_graph.objects.len(),
14054 7,
14055 "{:#?}",
14056 scene_delta.new_graph.objects
14057 );
14058
14059 ctx.close().await;
14060 }
14061
14062 #[tokio::test(flavor = "multi_thread")]
14063 async fn test_segments_symmetric() {
14064 let initial_source = "\
14065sketch(on = XY) {
14066 line(start = [var 0, var 0], end = [var 0, var 4])
14067 line(start = [var 4, var 0], end = [var 4, var 4])
14068 line(start = [var 2, var -1], end = [var 2, var 5])
14069}
14070";
14071
14072 let program = Program::parse(initial_source).unwrap().0.unwrap();
14073
14074 let mut frontend = FrontendState::new();
14075
14076 let ctx = ExecutorContext::new_mock(None).await;
14077 let version = Version(0);
14078
14079 frontend.program = program.clone();
14080 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14081 frontend.update_state_after_exec(outcome, true);
14082 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14083 let sketch_id = sketch_object.id;
14084 let sketch = expect_sketch(sketch_object);
14085 let line1_id = *sketch.segments.get(2).unwrap();
14086 let line2_id = *sketch.segments.get(5).unwrap();
14087 let axis_id = *sketch.segments.get(8).unwrap();
14088
14089 let constraint = Constraint::Symmetric(Symmetric {
14090 input: vec![line1_id, line2_id],
14091 axis: axis_id,
14092 });
14093 let (src_delta, scene_delta) = frontend
14094 .add_constraint(&ctx, version, sketch_id, constraint)
14095 .await
14096 .unwrap();
14097 insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14098 assert_eq!(
14099 scene_delta.new_graph.objects.len(),
14100 12,
14101 "{:#?}",
14102 scene_delta.new_graph.objects
14103 );
14104
14105 ctx.close().await;
14106 }
14107
14108 #[tokio::test(flavor = "multi_thread")]
14109 async fn test_point_arc_midpoint() {
14110 let initial_source = "\
14111sketch(on = XY) {
14112 point(at = [var 6, var 3])
14113 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14114}
14115";
14116
14117 let program = Program::parse(initial_source).unwrap().0.unwrap();
14118
14119 let mut frontend = FrontendState::new();
14120
14121 let ctx = ExecutorContext::new_mock(None).await;
14122 let version = Version(0);
14123
14124 frontend.program = program.clone();
14125 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14126 frontend.update_state_after_exec(outcome, true);
14127 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14128 let sketch_id = sketch_object.id;
14129 let sketch = expect_sketch(sketch_object);
14130 let point_id = *sketch.segments.first().unwrap();
14131 let arc_id = *sketch.segments.get(4).unwrap();
14132
14133 let constraint = Constraint::Midpoint(Midpoint {
14134 point: ConstraintSegment::from(point_id),
14135 segment: arc_id,
14136 });
14137 let (src_delta, scene_delta) = frontend
14138 .add_constraint(&ctx, version, sketch_id, constraint)
14139 .await
14140 .unwrap();
14141 insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14142 assert_eq!(
14143 scene_delta.new_graph.objects.len(),
14144 8,
14145 "{:#?}",
14146 scene_delta.new_graph.objects
14147 );
14148
14149 ctx.close().await;
14150 }
14151
14152 #[tokio::test(flavor = "multi_thread")]
14153 async fn test_origin_line_midpoint() {
14154 let initial_source = "\
14155sketch(on = XY) {
14156 line(start = [var 0, var 0], end = [var 6, var 4])
14157}
14158";
14159
14160 let program = Program::parse(initial_source).unwrap().0.unwrap();
14161
14162 let mut frontend = FrontendState::new();
14163
14164 let ctx = ExecutorContext::new_mock(None).await;
14165 let version = Version(0);
14166
14167 frontend.program = program.clone();
14168 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14169 frontend.update_state_after_exec(outcome, true);
14170 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14171 let sketch_id = sketch_object.id;
14172 let sketch = expect_sketch(sketch_object);
14173 let line_id = *sketch.segments.get(2).unwrap();
14174
14175 let constraint = Constraint::Midpoint(Midpoint {
14176 point: ConstraintSegment::ORIGIN,
14177 segment: line_id,
14178 });
14179 let (src_delta, scene_delta) = frontend
14180 .add_constraint(&ctx, version, sketch_id, constraint)
14181 .await
14182 .unwrap();
14183 insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14184 assert_eq!(
14185 scene_delta.new_graph.objects.len(),
14186 6,
14187 "{:#?}",
14188 scene_delta.new_graph.objects
14189 );
14190
14191 ctx.close().await;
14192 }
14193
14194 #[tokio::test(flavor = "multi_thread")]
14195 async fn test_origin_arc_midpoint() {
14196 let initial_source = "\
14197sketch(on = XY) {
14198 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14199}
14200";
14201
14202 let program = Program::parse(initial_source).unwrap().0.unwrap();
14203
14204 let mut frontend = FrontendState::new();
14205
14206 let ctx = ExecutorContext::new_mock(None).await;
14207 let version = Version(0);
14208
14209 frontend.program = program.clone();
14210 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14211 frontend.update_state_after_exec(outcome, true);
14212 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14213 let sketch_id = sketch_object.id;
14214 let sketch = expect_sketch(sketch_object);
14215 let arc_id = *sketch.segments.get(3).unwrap();
14216
14217 let constraint = Constraint::Midpoint(Midpoint {
14218 point: ConstraintSegment::ORIGIN,
14219 segment: arc_id,
14220 });
14221 let (src_delta, scene_delta) = frontend
14222 .add_constraint(&ctx, version, sketch_id, constraint)
14223 .await
14224 .unwrap();
14225 insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14226 assert_eq!(
14227 scene_delta.new_graph.objects.len(),
14228 7,
14229 "{:#?}",
14230 scene_delta.new_graph.objects
14231 );
14232
14233 ctx.close().await;
14234 }
14235
14236 #[tokio::test(flavor = "multi_thread")]
14237 async fn test_segments_symmetric_arcs() {
14238 let initial_source = "\
14239sketch(on = XY) {
14240 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14241 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14242 line(start = [var 0, var -10], end = [var 0, var 10])
14243}
14244";
14245
14246 let program = Program::parse(initial_source).unwrap().0.unwrap();
14247
14248 let mut frontend = FrontendState::new();
14249
14250 let ctx = ExecutorContext::new_mock(None).await;
14251 let version = Version(0);
14252
14253 frontend.program = program.clone();
14254 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14255 frontend.update_state_after_exec(outcome, true);
14256 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14257 let sketch_id = sketch_object.id;
14258 let sketch = expect_sketch(sketch_object);
14259 let arc1_id = *sketch.segments.get(3).unwrap();
14260 let arc2_id = *sketch.segments.get(7).unwrap();
14261 let axis_id = *sketch.segments.get(10).unwrap();
14262
14263 let constraint = Constraint::Symmetric(Symmetric {
14264 input: vec![arc1_id, arc2_id],
14265 axis: axis_id,
14266 });
14267 let (src_delta, scene_delta) = frontend
14268 .add_constraint(&ctx, version, sketch_id, constraint)
14269 .await
14270 .unwrap();
14271 insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14272 assert_eq!(
14273 scene_delta.new_graph.objects.len(),
14274 14,
14275 "{:#?}",
14276 scene_delta.new_graph.objects
14277 );
14278
14279 ctx.close().await;
14280 }
14281
14282 #[tokio::test(flavor = "multi_thread")]
14283 async fn test_sketch_on_face_simple() {
14284 let initial_source = "\
14285len = 2mm
14286cube = startSketchOn(XY)
14287 |> startProfile(at = [0, 0])
14288 |> line(end = [len, 0], tag = $side)
14289 |> line(end = [0, len])
14290 |> line(end = [-len, 0])
14291 |> line(end = [0, -len])
14292 |> close()
14293 |> extrude(length = len)
14294
14295face = faceOf(cube, face = side)
14296";
14297
14298 let program = Program::parse(initial_source).unwrap().0.unwrap();
14299
14300 let mut frontend = FrontendState::new();
14301
14302 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14303 let mock_ctx = ExecutorContext::new_mock(None).await;
14304 let version = Version(0);
14305
14306 frontend.hack_set_program(&ctx, program).await.unwrap();
14307 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14308 let face_id = face_object.id;
14309
14310 let sketch_args = SketchCtor {
14311 on: Plane::Object(face_id),
14312 };
14313 let (_src_delta, scene_delta, sketch_id) = frontend
14314 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14315 .await
14316 .unwrap();
14317 assert_eq!(sketch_id, ObjectId(2));
14318 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14319 let sketch_object = &scene_delta.new_graph.objects[2];
14320 assert_eq!(sketch_object.id, ObjectId(2));
14321 assert_eq!(
14322 sketch_object.kind,
14323 ObjectKind::Sketch(Sketch {
14324 args: SketchCtor {
14325 on: Plane::Object(face_id),
14326 },
14327 plane: face_id,
14328 segments: vec![],
14329 constraints: vec![],
14330 })
14331 );
14332 assert_eq!(scene_delta.new_graph.objects.len(), 8);
14333
14334 ctx.close().await;
14335 mock_ctx.close().await;
14336 }
14337
14338 #[tokio::test(flavor = "multi_thread")]
14339 async fn test_new_sketch_on_primitive_index_face() {
14340 let initial_source = "\
14341@settings(kclVersion = 2.0)
14342
14343sketch001 = sketch(on = XY) {
14344 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14345}
14346extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14347shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14348 let program = Program::parse(initial_source).unwrap().0.unwrap();
14349 let ctx = ExecutorContext::new_mock(None).await;
14350 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14351 let solid_id = match outcome.variables.get("shell001") {
14352 Some(KclValueView::Solid { value }) => value.id,
14353 value => panic!("expected shell001 to be a solid, got {value:?}"),
14354 };
14355 let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14356
14357 let mut ast = program.ast;
14358 let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14359 let face_expr = sketch_on_ast_expr(
14360 &mut ast,
14361 &scene_graph,
14362 &solid_references,
14363 &Plane::PrimitiveFace(crate::frontend::api::PrimitiveFacePlane { solid_id, index: 6 }),
14364 )
14365 .unwrap();
14366 let face_decl = ast::VariableDeclaration::new(
14367 ast::VariableDeclarator::new("face001", face_expr),
14368 ast::ItemVisibility::Default,
14369 ast::VariableKind::Const,
14370 );
14371 ast.body
14372 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14373 face_decl,
14374 ))));
14375 let face_source = source_from_ast(&ast);
14376 let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14377 insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14378
14379 let program = Program::parse(&new_source).unwrap().0.unwrap();
14380 ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14381 ctx.close().await;
14382 }
14383
14384 #[tokio::test(flavor = "multi_thread")]
14385 async fn test_sketch_on_wall_artifact_from_region_extrude() {
14386 let initial_source = "\
14387s = sketch(on = YZ) {
14388 line1 = line(start = [0, 0], end = [0, 1])
14389 line2 = line(start = [0, 1], end = [1, 1])
14390 line3 = line(start = [1, 1], end = [0, 0])
14391}
14392region001 = region(point = [0.1, 0.1], sketch = s)
14393extrude001 = extrude(region001, length = 5)
14394";
14395
14396 let program = Program::parse(initial_source).unwrap().0.unwrap();
14397
14398 let mut frontend = FrontendState::new();
14399 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14400 let version = Version(0);
14401
14402 frontend.hack_set_program(&ctx, program).await.unwrap();
14403 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14404
14405 let sketch_args = SketchCtor {
14406 on: Plane::Object(wall_object_id),
14407 };
14408 let (src_delta, _scene_delta, _sketch_id) = frontend
14409 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14410 .await
14411 .unwrap();
14412 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14413
14414 ctx.close().await;
14415 }
14416
14417 #[tokio::test(flavor = "multi_thread")]
14418 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14419 let initial_source = "\
14420sketch001 = sketch(on = YZ) {
14421 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14422 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14423 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14424 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14425 coincident([line1.end, line2.start])
14426 coincident([line2.end, line3.start])
14427 coincident([line3.end, line4.start])
14428 coincident([line4.end, line1.start])
14429 parallel([line2, line4])
14430 parallel([line3, line1])
14431 perpendicular([line1, line2])
14432 horizontal(line3)
14433 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14434}
14435region001 = region(point = [3.1, 3.74], sketch = sketch001)
14436extrude001 = extrude(region001, length = 5)
14437";
14438
14439 let program = Program::parse(initial_source).unwrap().0.unwrap();
14440
14441 let mut frontend = FrontendState::new();
14442 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14443 let version = Version(0);
14444
14445 frontend.hack_set_program(&ctx, program).await.unwrap();
14446 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14447
14448 let sketch_args = SketchCtor {
14449 on: Plane::Object(wall_object_id),
14450 };
14451 let (src_delta, _scene_delta, _sketch_id) = frontend
14452 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14453 .await
14454 .unwrap();
14455 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14456
14457 ctx.close().await;
14458 }
14459
14460 #[tokio::test(flavor = "multi_thread")]
14461 async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14462 let initial_source = "\
14463@settings(kclVersion = 2.0)
14464
14465sketch001 = sketch(on = XY) {
14466 circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14467 circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14468}
14469hidden001 = hide(sketch001)
14470region001 = region(segments = [sketch001.circle2])
14471region002 = region(segments = [sketch001.circle1])
14472extrude001 = extrude([region001, region002], length = 5)
14473";
14474
14475 let program = Program::parse(initial_source).unwrap().0.unwrap();
14476 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14477 let version = Version(0);
14478
14479 for (solid_output_index, expected_face) in [
14480 (0, "faceOf(extrude001[0], face = END)"),
14481 (1, "faceOf(extrude001[1], face = END)"),
14482 ] {
14483 let mut frontend = FrontendState::new();
14484 frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14485 let cap_object_id = find_cap_object_id_with_solid_output_index(
14486 &frontend.scene_graph,
14487 crate::frontend::api::CapKind::End,
14488 solid_output_index,
14489 )
14490 .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14491
14492 let sketch_args = SketchCtor {
14493 on: Plane::Object(cap_object_id),
14494 };
14495 let (src_delta, _scene_delta, _sketch_id) = frontend
14496 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14497 .await
14498 .unwrap();
14499
14500 assert!(
14501 src_delta.text.contains(expected_face),
14502 "expected `{expected_face}` in:\n{}",
14503 src_delta.text
14504 );
14505 assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14506 }
14507
14508 ctx.close().await;
14509 }
14510
14511 #[tokio::test(flavor = "multi_thread")]
14512 async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14513 let initial_source = "\
14514@settings(kclVersion = 2.0)
14515
14516sketch001 = sketch(on = XY) {
14517 rect1Line1 = line(start = [0, 0], end = [1, 0])
14518 rect1Line2 = line(start = [1, 0], end = [1, 1])
14519 rect1Line3 = line(start = [1, 1], end = [0, 1])
14520 rect1Line4 = line(start = [0, 1], end = [0, 0])
14521 rect2Line1 = line(start = [3, 0], end = [4, 0])
14522 rect2Line2 = line(start = [4, 0], end = [4, 1])
14523 rect2Line3 = line(start = [4, 1], end = [3, 1])
14524 rect2Line4 = line(start = [3, 1], end = [3, 0])
14525}
14526hidden001 = hide(sketch001)
14527region001 = region(segments = [
14528 sketch001.rect1Line4,
14529 sketch001.rect1Line1
14530])
14531region002 = region(segments = [
14532 sketch001.rect2Line4,
14533 sketch001.rect2Line1
14534])
14535extrude001 = extrude([region001, region002], length = 5)
14536";
14537
14538 let program = Program::parse(initial_source).unwrap().0.unwrap();
14539 let mut frontend = FrontendState::new();
14540 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14541 let version = Version(0);
14542
14543 frontend.hack_set_program(&ctx, program).await.unwrap();
14544 let region_call = "\
14545region(segments = [
14546 sketch001.rect1Line4,
14547 sketch001.rect1Line1
14548])";
14549 let region_call_start = initial_source.find(region_call).unwrap();
14550 let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14551 let segment_call = "line(start = [0, 0], end = [1, 0])";
14552 let segment_call_start = initial_source.find(segment_call).unwrap();
14553 let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14554 let wall_object_id = frontend
14555 .scene_graph
14556 .objects
14557 .iter()
14558 .find_map(|object| match &object.kind {
14559 ObjectKind::Wall(wall)
14560 if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14561 && wall.source.segment.range == segment_range =>
14562 {
14563 Some(object.id)
14564 }
14565 _ => None,
14566 })
14567 .expect("expected a wall object for region001.tags.rect1Line1");
14568
14569 let sketch_args = SketchCtor {
14570 on: Plane::Object(wall_object_id),
14571 };
14572 let (src_delta, _scene_delta, _sketch_id) = frontend
14573 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14574 .await
14575 .unwrap();
14576
14577 let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14578 assert!(
14579 src_delta.text.contains(expected_face),
14580 "expected `{expected_face}` in:\n{}",
14581 src_delta.text
14582 );
14583 assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14584
14585 ctx.close().await;
14586 }
14587
14588 #[test]
14589 fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14590 let source = "\
14591sketch001 = sketch(on = XY) {
14592 line(start = [0, 0], end = [1, 0])
14593}
14594part = subtract(boxSolid, tools = [cutSolid])
14595 |> appearance(color = \"#8f96a3\")
14596";
14597 let ast = Program::parse(source).unwrap().0.unwrap().ast;
14598 let line_start = source.find("line").unwrap();
14599 let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14600 let line_ref = SourceRef::Simple {
14601 range: [line_start, line_end, 0].into(),
14602 node_path: None,
14603 };
14604 assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14605
14606 let subtract_start = source.find("subtract").unwrap();
14607 let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14608 let subtract_ref = SourceRef::Simple {
14609 range: [subtract_start, subtract_end, 0].into(),
14610 node_path: None,
14611 };
14612 assert_eq!(
14613 variable_name_containing_source_ref(&ast, &subtract_ref),
14614 Some("part".to_owned())
14615 );
14616 }
14617
14618 #[tokio::test(flavor = "multi_thread")]
14619 async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14620 clear_mem_cache().await;
14621 let source = "\
14622boxSolid = startSketchOn(XY)
14623 |> startProfile(at = [0, 0])
14624 |> line(end = [4, 0], tag = $bottomEdge)
14625 |> line(end = [0, 4])
14626 |> line(end = [-4, 0])
14627 |> close()
14628 |> extrude(length = 10)
14629cutSolid = startSketchOn(XY)
14630 |> startProfile(at = [1, 1])
14631 |> line(end = [1, 0])
14632 |> line(end = [0, 1])
14633 |> line(end = [-1, 0])
14634 |> close()
14635 |> extrude(length = 10)
14636part = subtract(boxSolid, tools = [cutSolid])
14637 |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14638";
14639 let program = Program::parse(source).unwrap().0.unwrap();
14640 let mut frontend = FrontendState::new();
14641 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14642 match frontend.hack_set_program(&ctx, program).await.unwrap() {
14643 SetProgramOutcome::Success { .. } => {}
14644 SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14645 }
14646
14647 let sweep_call_start = source.find("extrude").unwrap();
14648 let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14649 let part_call_start = source.find("subtract").unwrap();
14650 let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14651 let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14652 let composite_range = [part_call_start, part_call_end, 0].into();
14653
14654 let cap_object = frontend
14655 .scene_graph
14656 .objects
14657 .iter()
14658 .find(|object| {
14659 matches!(
14660 &object.kind,
14661 ObjectKind::Cap(crate::frontend::api::Cap {
14662 kind: crate::frontend::api::CapKind::End,
14663 source,
14664 ..
14665 }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14666 )
14667 })
14668 .expect("expected end cap object to trace through subtract and original extrude");
14669
14670 let mut ast = frontend.program.ast.clone();
14671 let cap_expr = sketch_on_ast_expr(
14672 &mut ast,
14673 &frontend.scene_graph,
14674 &frontend.solid_references,
14675 &Plane::Object(cap_object.id),
14676 )
14677 .unwrap();
14678 let cap_face_decl = ast::VariableDeclaration::new(
14679 ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14680 ast::ItemVisibility::Default,
14681 ast::VariableKind::Const,
14682 );
14683 ast.body
14684 .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14685 cap_face_decl,
14686 ))));
14687 let generated_source = source_from_ast(&ast);
14688
14689 assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14690 assert!(!generated_source.contains("faceOf(boxSolid"));
14691 let ast::Expr::CallExpressionKw(call) = cap_expr else {
14692 panic!("expected faceOf call");
14693 };
14694 assert_eq!(call.callee.name.name, "faceOf");
14695 let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14696 panic!("expected solid name");
14697 };
14698 assert_eq!(solid_name.name.name, "part");
14699 let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14700 panic!("expected face name");
14701 };
14702 assert_eq!(face_name.name.name, "END");
14703
14704 ctx.close().await;
14705 }
14706
14707 #[tokio::test(flavor = "multi_thread")]
14708 async fn test_sketch_on_plane_incremental() {
14709 let initial_source = "\
14710len = 2mm
14711cube = startSketchOn(XY)
14712 |> startProfile(at = [0, 0])
14713 |> line(end = [len, 0], tag = $side)
14714 |> line(end = [0, len])
14715 |> line(end = [-len, 0])
14716 |> line(end = [0, -len])
14717 |> close()
14718 |> extrude(length = len)
14719
14720plane = planeOf(cube, face = side)
14721";
14722
14723 let program = Program::parse(initial_source).unwrap().0.unwrap();
14724
14725 let mut frontend = FrontendState::new();
14726
14727 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14728 let mock_ctx = ExecutorContext::new_mock(None).await;
14729 let version = Version(0);
14730
14731 frontend.hack_set_program(&ctx, program).await.unwrap();
14732 let plane_object = frontend
14734 .scene_graph
14735 .objects
14736 .iter()
14737 .rev()
14738 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14739 .unwrap();
14740 let plane_id = plane_object.id;
14741
14742 let sketch_args = SketchCtor {
14743 on: Plane::Object(plane_id),
14744 };
14745 let (src_delta, scene_delta, sketch_id) = frontend
14746 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14747 .await
14748 .unwrap();
14749 insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14750 assert_eq!(sketch_id, ObjectId(2));
14751 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14752 let sketch_object = &scene_delta.new_graph.objects[2];
14753 assert_eq!(sketch_object.id, ObjectId(2));
14754 assert_eq!(
14755 sketch_object.kind,
14756 ObjectKind::Sketch(Sketch {
14757 args: SketchCtor {
14758 on: Plane::Object(plane_id),
14759 },
14760 plane: plane_id,
14761 segments: vec![],
14762 constraints: vec![],
14763 })
14764 );
14765 assert_eq!(scene_delta.new_graph.objects.len(), 9);
14766
14767 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14768 assert_eq!(plane_object.id, plane_id);
14769 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14770
14771 ctx.close().await;
14772 mock_ctx.close().await;
14773 }
14774
14775 #[tokio::test(flavor = "multi_thread")]
14776 async fn test_new_sketch_uses_unique_variable_name() {
14777 let initial_source = "\
14778sketch1 = sketch(on = XY) {
14779}
14780";
14781
14782 let program = Program::parse(initial_source).unwrap().0.unwrap();
14783
14784 let mut frontend = FrontendState::new();
14785 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14786 let version = Version(0);
14787
14788 frontend.hack_set_program(&ctx, program).await.unwrap();
14789
14790 let sketch_args = SketchCtor {
14791 on: Plane::Default(PlaneName::Yz),
14792 };
14793 let (src_delta, _, _) = frontend
14794 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14795 .await
14796 .unwrap();
14797
14798 insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14799
14800 ctx.close().await;
14801 }
14802
14803 #[tokio::test(flavor = "multi_thread")]
14804 async fn test_new_sketch_twice_using_same_plane() {
14805 let initial_source = "\
14806sketch1 = sketch(on = XY) {
14807}
14808";
14809
14810 let program = Program::parse(initial_source).unwrap().0.unwrap();
14811
14812 let mut frontend = FrontendState::new();
14813 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14814 let version = Version(0);
14815
14816 frontend.hack_set_program(&ctx, program).await.unwrap();
14817
14818 let sketch_args = SketchCtor {
14819 on: Plane::Default(PlaneName::Xy),
14820 };
14821 let (src_delta, _, _) = frontend
14822 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14823 .await
14824 .unwrap();
14825
14826 insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14827
14828 ctx.close().await;
14829 }
14830
14831 #[tokio::test(flavor = "multi_thread")]
14832 async fn test_sketch_mode_reuses_cached_on_expression() {
14833 let initial_source = "\
14834width = 2mm
14835sketch(on = offsetPlane(XY, offset = width)) {
14836 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14837 distance([line1.start, line1.end]) == width
14838}
14839";
14840 let program = Program::parse(initial_source).unwrap().0.unwrap();
14841
14842 let mut frontend = FrontendState::new();
14843 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14844 let mock_ctx = ExecutorContext::new_mock(None).await;
14845 let version = Version(0);
14846 let project_id = ProjectId(0);
14847 let file_id = FileId(0);
14848
14849 frontend.hack_set_program(&ctx, program).await.unwrap();
14850 let initial_object_count = frontend.scene_graph.objects.len();
14851 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14852 .expect("Expected sketch object to exist")
14853 .id;
14854
14855 let scene_delta = frontend
14858 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14859 .await
14860 .unwrap();
14861 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14862
14863 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14866 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14867
14868 ctx.close().await;
14869 mock_ctx.close().await;
14870 }
14871
14872 #[tokio::test(flavor = "multi_thread")]
14873 async fn test_edit_sketch_nested_in_pipe() {
14874 clear_mem_cache().await;
14875 let source = r#"
14876profile = sketch(on = XY) {
14877 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14878}
14879 |> translate(x = 2mm)
14880"#;
14881 let program = Program::parse_no_errs(source).unwrap();
14882 let mut frontend = FrontendState::new();
14883 let mock_ctx = ExecutorContext::new_mock(None).await;
14884 let version = Version(0);
14885
14886 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14887 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14888 .expect("Expected piped sketch object")
14889 .id;
14890
14891 let scene_delta = frontend
14892 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14893 .await
14894 .unwrap();
14895 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14896 assert!(
14897 scene_delta
14898 .new_graph
14899 .objects
14900 .iter()
14901 .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14902 "Expected the piped sketch's segments to be present in sketch mode"
14903 );
14904
14905 clear_mem_cache().await;
14906 mock_ctx.close().await;
14907 }
14908
14909 #[tokio::test(flavor = "multi_thread")]
14910 async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14911 clear_mem_cache().await;
14912 let source = r#"
14913useFirstProfile = true
14914
14915profile = if useFirstProfile {
14916 sketch(on = XY) {
14917 line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14918 }
14919} else {
14920 sketch(on = XY) {
14921 line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14922 }
14923}
14924"#;
14925 let program = Program::parse_no_errs(source).unwrap();
14926 let mut frontend = FrontendState::new();
14927 let mock_ctx = ExecutorContext::new_mock(None).await;
14928 let version = Version(0);
14929
14930 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14931 let sketch_object =
14932 find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14933 let sketch_id = sketch_object.id;
14934 let sketch = expect_sketch(sketch_object);
14935 let line_end_id = *sketch
14936 .segments
14937 .get(1)
14938 .expect("Expected the active branch's line end point");
14939
14940 let scene_delta = frontend
14941 .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14942 .await
14943 .unwrap();
14944 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14945
14946 let segments = vec![ExistingSegmentCtor {
14947 id: line_end_id,
14948 ctor: SegmentCtor::Point(PointCtor {
14949 position: Point2d {
14950 x: Expr::Var(Number {
14951 value: 30.0,
14952 units: NumericSuffix::Mm,
14953 }),
14954 y: Expr::Var(Number {
14955 value: 15.0,
14956 units: NumericSuffix::Mm,
14957 }),
14958 },
14959 }),
14960 }];
14961 let (source_delta, _) = frontend
14962 .edit_segments(&mock_ctx, version, sketch_id, segments)
14963 .await
14964 .unwrap();
14965 assert!(
14966 source_delta
14967 .text
14968 .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14969 "Expected the active branch's dragged variables to be updated:\n{}",
14970 source_delta.text
14971 );
14972 assert!(
14973 source_delta
14974 .text
14975 .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14976 "Expected the inactive branch to remain unchanged:\n{}",
14977 source_delta.text
14978 );
14979
14980 clear_mem_cache().await;
14981 mock_ctx.close().await;
14982 }
14983
14984 #[tokio::test(flavor = "multi_thread")]
14985 async fn test_multiple_sketch_blocks() {
14986 let initial_source = "\
14987// Cube that requires the engine.
14988width = 2
14989sketch001 = startSketchOn(XY)
14990profile001 = startProfile(sketch001, at = [0, 0])
14991 |> yLine(length = width, tag = $seg1)
14992 |> xLine(length = width)
14993 |> yLine(length = -width)
14994 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14995 |> close()
14996extrude001 = extrude(profile001, length = width)
14997
14998// Get a value that requires the engine.
14999x = segLen(seg1)
15000
15001// Triangle with side length 2*x.
15002sketch(on = XY) {
15003 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
15004 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
15005 coincident([line1.end, line2.start])
15006 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
15007 coincident([line2.end, line3.start])
15008 coincident([line3.end, line1.start])
15009 equalLength([line3, line1])
15010 equalLength([line1, line2])
15011 distance([line1.start, line1.end]) == 2*x
15012}
15013
15014// Line segment with length x.
15015sketch2 = sketch(on = XY) {
15016 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
15017 distance([line1.start, line1.end]) == x
15018}
15019";
15020
15021 let program = Program::parse(initial_source).unwrap().0.unwrap();
15022
15023 let mut frontend = FrontendState::new();
15024
15025 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15026 let mock_ctx = ExecutorContext::new_mock(None).await;
15027 let version = Version(0);
15028 let project_id = ProjectId(0);
15029 let file_id = FileId(0);
15030
15031 frontend.hack_set_program(&ctx, program).await.unwrap();
15032 let sketch_objects = frontend
15033 .scene_graph
15034 .objects
15035 .iter()
15036 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
15037 .collect::<Vec<_>>();
15038 let sketch1_id = sketch_objects.first().unwrap().id;
15039 let sketch2_id = sketch_objects.get(1).unwrap().id;
15040 let point1_id = ObjectId(sketch1_id.0 + 1);
15042 let point2_id = ObjectId(sketch2_id.0 + 1);
15044
15045 let scene_delta = frontend
15054 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15055 .await
15056 .unwrap();
15057 assert_eq!(
15058 scene_delta.new_graph.objects.len(),
15059 18,
15060 "{:#?}",
15061 scene_delta.new_graph.objects
15062 );
15063
15064 let point_ctor = PointCtor {
15066 position: Point2d {
15067 x: Expr::Var(Number {
15068 value: 1.0,
15069 units: NumericSuffix::Mm,
15070 }),
15071 y: Expr::Var(Number {
15072 value: 2.0,
15073 units: NumericSuffix::Mm,
15074 }),
15075 },
15076 };
15077 let segments = vec![ExistingSegmentCtor {
15078 id: point1_id,
15079 ctor: SegmentCtor::Point(point_ctor),
15080 }];
15081 let (src_delta, _) = frontend
15082 .edit_segments(&mock_ctx, version, sketch1_id, segments)
15083 .await
15084 .unwrap();
15085 insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15087 let edited_sketch1_source = src_delta.text.clone();
15088
15089 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15091 assert_eq!(src_delta.text, edited_sketch1_source);
15092 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15100 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15101
15102 let scene_delta = frontend
15110 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15111 .await
15112 .unwrap();
15113 assert_eq!(
15114 scene_delta.new_graph.objects.len(),
15115 24,
15116 "{:#?}",
15117 scene_delta.new_graph.objects
15118 );
15119
15120 let point_ctor = PointCtor {
15122 position: Point2d {
15123 x: Expr::Var(Number {
15124 value: 3.0,
15125 units: NumericSuffix::Mm,
15126 }),
15127 y: Expr::Var(Number {
15128 value: 4.0,
15129 units: NumericSuffix::Mm,
15130 }),
15131 },
15132 };
15133 let segments = vec![ExistingSegmentCtor {
15134 id: point2_id,
15135 ctor: SegmentCtor::Point(point_ctor),
15136 }];
15137 let (src_delta, _) = frontend
15138 .edit_segments(&mock_ctx, version, sketch2_id, segments)
15139 .await
15140 .unwrap();
15141 insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15143 let edited_sketch2_source = src_delta.text.clone();
15144
15145 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15147 assert_eq!(src_delta.text, edited_sketch2_source);
15148
15149 ctx.close().await;
15150 mock_ctx.close().await;
15151 }
15152
15153 #[tokio::test(flavor = "multi_thread")]
15154 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15155 clear_mem_cache().await;
15156
15157 let source = r#"sketch001 = sketch(on = XZ) {
15158 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15159}
15160sketch002 = sketch(on = XY) {
15161 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15162 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15163 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15164 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15165 coincident([line1.end, line2.start])
15166 coincident([line2.end, line3.start])
15167 coincident([line3.end, line4.start])
15168 coincident([line4.end, line1.start])
15169 parallel([line2, line4])
15170 parallel([line3, line1])
15171 perpendicular([line1, line2])
15172 horizontal(line3)
15173 coincident([line1.start, ORIGIN])
15174}
15175"#;
15176
15177 let program = Program::parse(source).unwrap().0.unwrap();
15178 let mut frontend = FrontendState::new();
15179 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15180 let mock_ctx = ExecutorContext::new_mock(None).await;
15181 let version = Version(0);
15182 let project_id = ProjectId(0);
15183 let file_id = FileId(0);
15184
15185 frontend.hack_set_program(&ctx, program).await.unwrap();
15186 let sketch_objects = frontend
15187 .scene_graph
15188 .objects
15189 .iter()
15190 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15191 .collect::<Vec<_>>();
15192 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15193
15194 let sketch1_id = sketch_objects[0].id;
15195 let sketch2_id = sketch_objects[1].id;
15196
15197 frontend
15198 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15199 .await
15200 .unwrap();
15201 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15202
15203 let scene_delta = frontend
15204 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15205 .await
15206 .unwrap();
15207 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15208
15209 clear_mem_cache().await;
15210 ctx.close().await;
15211 mock_ctx.close().await;
15212 }
15213
15214 #[tokio::test(flavor = "multi_thread")]
15219 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15220 let initial_source = "@settings(defaultLengthUnit = mm)
15222
15223sketch001 = sketch(on = XY) {
15224 point(at = [1in, 2in])
15225}
15226";
15227
15228 let program = Program::parse(initial_source).unwrap().0.unwrap();
15229 let mut frontend = FrontendState::new();
15230
15231 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15232 let mock_ctx = ExecutorContext::new_mock(None).await;
15233 let version = Version(0);
15234 let project_id = ProjectId(0);
15235 let file_id = FileId(0);
15236
15237 frontend.hack_set_program(&ctx, program).await.unwrap();
15238 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15239 let sketch_id = sketch_object.id;
15240
15241 frontend
15243 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15244 .await
15245 .unwrap();
15246
15247 let point_ctor = PointCtor {
15249 position: Point2d {
15250 x: Expr::Number(Number {
15251 value: 5.0,
15252 units: NumericSuffix::Mm,
15253 }),
15254 y: Expr::Number(Number {
15255 value: 6.0,
15256 units: NumericSuffix::Mm,
15257 }),
15258 },
15259 };
15260 let segment = SegmentCtor::Point(point_ctor);
15261 let (src_delta, scene_delta) = frontend
15262 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15263 .await
15264 .unwrap();
15265 assert!(
15267 src_delta.text.contains("point(at = [5mm, 6mm])"),
15268 "Expected new point in source, got: {}",
15269 src_delta.text
15270 );
15271 assert!(!scene_delta.new_objects.is_empty());
15272
15273 ctx.close().await;
15274 mock_ctx.close().await;
15275 }
15276
15277 #[tokio::test(flavor = "multi_thread")]
15278 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15279 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15280
15281 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15282 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15283
15284 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15285 assert!(
15286 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15287 "Expected experimental settings to be added to source"
15288 );
15289 }
15290
15291 #[tokio::test(flavor = "multi_thread")]
15292 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15293 let initial_source = "@settings(defaultLengthUnit = mm)
15295
15296s = sketch(on = XY) {}
15297";
15298
15299 let program = Program::parse(initial_source).unwrap().0.unwrap();
15300 let mut frontend = FrontendState::new();
15301
15302 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15303 let mock_ctx = ExecutorContext::new_mock(None).await;
15304 let version = Version(0);
15305
15306 frontend.hack_set_program(&ctx, program).await.unwrap();
15307 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15308 let sketch_id = sketch_object.id;
15309
15310 let line_ctor = LineCtor {
15311 start: Point2d {
15312 x: Expr::Number(Number {
15313 value: 0.0,
15314 units: NumericSuffix::Mm,
15315 }),
15316 y: Expr::Number(Number {
15317 value: 0.0,
15318 units: NumericSuffix::Mm,
15319 }),
15320 },
15321 end: Point2d {
15322 x: Expr::Number(Number {
15323 value: 10.0,
15324 units: NumericSuffix::Mm,
15325 }),
15326 y: Expr::Number(Number {
15327 value: 10.0,
15328 units: NumericSuffix::Mm,
15329 }),
15330 },
15331 construction: None,
15332 };
15333 let segment = SegmentCtor::Line(line_ctor);
15334 let (src_delta, scene_delta) = frontend
15335 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15336 .await
15337 .unwrap();
15338 assert!(
15339 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15340 "Expected line in source, got: {}",
15341 src_delta.text
15342 );
15343 assert_eq!(scene_delta.new_objects.len(), 3);
15345
15346 ctx.close().await;
15347 mock_ctx.close().await;
15348 }
15349
15350 #[tokio::test(flavor = "multi_thread")]
15351 async fn test_extra_newlines_between_operations_edit_line() {
15352 let initial_source = "@settings(defaultLengthUnit = mm)
15354
15355sketch001 = sketch(on = XY) {
15356
15357 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15358
15359}
15360";
15361
15362 let program = Program::parse(initial_source).unwrap().0.unwrap();
15363 let mut frontend = FrontendState::new();
15364
15365 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15366 let mock_ctx = ExecutorContext::new_mock(None).await;
15367 let version = Version(0);
15368 let project_id = ProjectId(0);
15369 let file_id = FileId(0);
15370
15371 frontend.hack_set_program(&ctx, program).await.unwrap();
15372 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15373 let sketch_id = sketch_object.id;
15374 let sketch = expect_sketch(sketch_object);
15375
15376 let line_id = sketch
15378 .segments
15379 .iter()
15380 .copied()
15381 .find(|seg_id| {
15382 matches!(
15383 &frontend.scene_graph.objects[seg_id.0].kind,
15384 ObjectKind::Segment {
15385 segment: Segment::Line(_)
15386 }
15387 )
15388 })
15389 .expect("Expected a line segment in sketch");
15390
15391 frontend
15393 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15394 .await
15395 .unwrap();
15396
15397 let line_ctor = LineCtor {
15399 start: Point2d {
15400 x: Expr::Var(Number {
15401 value: 1.0,
15402 units: NumericSuffix::Mm,
15403 }),
15404 y: Expr::Var(Number {
15405 value: 2.0,
15406 units: NumericSuffix::Mm,
15407 }),
15408 },
15409 end: Point2d {
15410 x: Expr::Var(Number {
15411 value: 13.0,
15412 units: NumericSuffix::Mm,
15413 }),
15414 y: Expr::Var(Number {
15415 value: 14.0,
15416 units: NumericSuffix::Mm,
15417 }),
15418 },
15419 construction: None,
15420 };
15421 let segments = vec![ExistingSegmentCtor {
15422 id: line_id,
15423 ctor: SegmentCtor::Line(line_ctor),
15424 }];
15425 let (src_delta, _scene_delta) = frontend
15426 .edit_segments(&mock_ctx, version, sketch_id, segments)
15427 .await
15428 .unwrap();
15429 assert!(
15430 src_delta
15431 .text
15432 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15433 "Expected edited line in source, got: {}",
15434 src_delta.text
15435 );
15436
15437 ctx.close().await;
15438 mock_ctx.close().await;
15439 }
15440
15441 #[tokio::test(flavor = "multi_thread")]
15442 async fn test_extra_newlines_delete_segment() {
15443 let initial_source = "@settings(defaultLengthUnit = mm)
15445
15446sketch001 = sketch(on = XY) {
15447 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15448}
15449";
15450
15451 let program = Program::parse(initial_source).unwrap().0.unwrap();
15452 let mut frontend = FrontendState::new();
15453
15454 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15455 let mock_ctx = ExecutorContext::new_mock(None).await;
15456 let version = Version(0);
15457
15458 frontend.hack_set_program(&ctx, program).await.unwrap();
15459 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15460 let sketch_id = sketch_object.id;
15461 let sketch = expect_sketch(sketch_object);
15462
15463 assert_eq!(sketch.segments.len(), 3);
15465 let circle_id = sketch.segments[2];
15466
15467 let (src_delta, scene_delta) = frontend
15469 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15470 .await
15471 .unwrap();
15472 assert!(
15473 src_delta.text.contains("sketch(on = XY) {"),
15474 "Expected sketch block in source, got: {}",
15475 src_delta.text
15476 );
15477 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15478 let new_sketch = expect_sketch(new_sketch_object);
15479 assert_eq!(new_sketch.segments.len(), 0);
15480
15481 ctx.close().await;
15482 mock_ctx.close().await;
15483 }
15484
15485 #[tokio::test(flavor = "multi_thread")]
15486 async fn test_unformatted_source_add_arc() {
15487 let initial_source = "@settings(defaultLengthUnit = mm)
15489
15490sketch001 = sketch(on = XY) {
15491}
15492";
15493
15494 let program = Program::parse(initial_source).unwrap().0.unwrap();
15495 let mut frontend = FrontendState::new();
15496
15497 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15498 let mock_ctx = ExecutorContext::new_mock(None).await;
15499 let version = Version(0);
15500
15501 frontend.hack_set_program(&ctx, program).await.unwrap();
15502 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15503 let sketch_id = sketch_object.id;
15504
15505 let arc_ctor = ArcCtor {
15506 start: Point2d {
15507 x: Expr::Var(Number {
15508 value: 5.0,
15509 units: NumericSuffix::Mm,
15510 }),
15511 y: Expr::Var(Number {
15512 value: 0.0,
15513 units: NumericSuffix::Mm,
15514 }),
15515 },
15516 end: Point2d {
15517 x: Expr::Var(Number {
15518 value: 0.0,
15519 units: NumericSuffix::Mm,
15520 }),
15521 y: Expr::Var(Number {
15522 value: 5.0,
15523 units: NumericSuffix::Mm,
15524 }),
15525 },
15526 center: Point2d {
15527 x: Expr::Var(Number {
15528 value: 0.0,
15529 units: NumericSuffix::Mm,
15530 }),
15531 y: Expr::Var(Number {
15532 value: 0.0,
15533 units: NumericSuffix::Mm,
15534 }),
15535 },
15536 direction: None,
15537 construction: None,
15538 };
15539 let segment = SegmentCtor::Arc(arc_ctor);
15540 let (src_delta, scene_delta) = frontend
15541 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15542 .await
15543 .unwrap();
15544 assert!(
15545 src_delta
15546 .text
15547 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15548 "Expected arc in source, got: {}",
15549 src_delta.text
15550 );
15551 assert!(!scene_delta.new_objects.is_empty());
15552
15553 ctx.close().await;
15554 mock_ctx.close().await;
15555 }
15556
15557 #[tokio::test(flavor = "multi_thread")]
15558 async fn test_arc_direction_flows_to_source() {
15559 let initial_source = "@settings(defaultLengthUnit = mm)
15560
15561sketch001 = sketch(on = XY) {
15562}
15563";
15564
15565 let program = Program::parse(initial_source).unwrap().0.unwrap();
15566 let mut frontend = FrontendState::new();
15567
15568 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15569 let mock_ctx = ExecutorContext::new_mock(None).await;
15570 let version = Version(0);
15571
15572 frontend.hack_set_program(&ctx, program).await.unwrap();
15573 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15574 let sketch_id = sketch_object.id;
15575
15576 let point = |x: f64, y: f64| Point2d {
15577 x: Expr::Var(Number {
15578 value: x,
15579 units: NumericSuffix::Mm,
15580 }),
15581 y: Expr::Var(Number {
15582 value: y,
15583 units: NumericSuffix::Mm,
15584 }),
15585 };
15586
15587 let arc_ctor = ArcCtor {
15589 start: point(5.0, 0.0),
15590 end: point(0.0, 5.0),
15591 center: point(0.0, 0.0),
15592 direction: Some(ArcDirection::Cw),
15593 construction: None,
15594 };
15595 let (src_delta, scene_delta) = frontend
15596 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15597 .await
15598 .unwrap();
15599 assert!(
15600 src_delta.text.contains("direction = CW"),
15601 "Expected direction = CW in source, got: {}",
15602 src_delta.text
15603 );
15604 let arc_id = *scene_delta.new_objects.last().unwrap();
15606
15607 let edited_ctor = ArcCtor {
15611 start: point(0.0, -5.0),
15612 end: point(0.0, 5.0),
15613 center: point(0.0, 0.0),
15614 direction: Some(ArcDirection::Cw),
15615 construction: None,
15616 };
15617 let (src_delta, _scene_delta) = frontend
15618 .edit_segments(
15619 &mock_ctx,
15620 version,
15621 sketch_id,
15622 vec![ExistingSegmentCtor {
15623 id: arc_id,
15624 ctor: SegmentCtor::Arc(edited_ctor),
15625 }],
15626 )
15627 .await
15628 .unwrap();
15629 assert!(
15630 src_delta.text.contains("start = [var 0mm, var -5mm]"),
15631 "Expected edited start point in source, got: {}",
15632 src_delta.text
15633 );
15634 assert!(
15635 src_delta.text.contains("direction = CW"),
15636 "Expected direction = CW to be preserved in source, got: {}",
15637 src_delta.text
15638 );
15639
15640 let edited_ctor = ArcCtor {
15643 start: point(0.0, -5.0),
15644 end: point(0.0, 5.0),
15645 center: point(0.0, 0.0),
15646 direction: Some(ArcDirection::Ccw),
15647 construction: None,
15648 };
15649 let (src_delta, _scene_delta) = frontend
15650 .edit_segments(
15651 &mock_ctx,
15652 version,
15653 sketch_id,
15654 vec![ExistingSegmentCtor {
15655 id: arc_id,
15656 ctor: SegmentCtor::Arc(edited_ctor),
15657 }],
15658 )
15659 .await
15660 .unwrap();
15661 assert!(
15662 !src_delta.text.contains("direction"),
15663 "Expected direction argument to be removed from source, got: {}",
15664 src_delta.text
15665 );
15666
15667 ctx.close().await;
15668 mock_ctx.close().await;
15669 }
15670
15671 #[tokio::test(flavor = "multi_thread")]
15672 async fn test_extra_newlines_add_circle() {
15673 let initial_source = "@settings(defaultLengthUnit = mm)
15675
15676sketch001 = sketch(on = XY) {
15677}
15678";
15679
15680 let program = Program::parse(initial_source).unwrap().0.unwrap();
15681 let mut frontend = FrontendState::new();
15682
15683 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15684 let mock_ctx = ExecutorContext::new_mock(None).await;
15685 let version = Version(0);
15686
15687 frontend.hack_set_program(&ctx, program).await.unwrap();
15688 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15689 let sketch_id = sketch_object.id;
15690
15691 let circle_ctor = CircleCtor {
15692 start: Point2d {
15693 x: Expr::Var(Number {
15694 value: 5.0,
15695 units: NumericSuffix::Mm,
15696 }),
15697 y: Expr::Var(Number {
15698 value: 0.0,
15699 units: NumericSuffix::Mm,
15700 }),
15701 },
15702 center: Point2d {
15703 x: Expr::Var(Number {
15704 value: 0.0,
15705 units: NumericSuffix::Mm,
15706 }),
15707 y: Expr::Var(Number {
15708 value: 0.0,
15709 units: NumericSuffix::Mm,
15710 }),
15711 },
15712 construction: None,
15713 };
15714 let segment = SegmentCtor::Circle(circle_ctor);
15715 let (src_delta, scene_delta) = frontend
15716 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15717 .await
15718 .unwrap();
15719 assert!(
15720 src_delta
15721 .text
15722 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15723 "Expected circle in source, got: {}",
15724 src_delta.text
15725 );
15726 assert!(!scene_delta.new_objects.is_empty());
15727
15728 ctx.close().await;
15729 mock_ctx.close().await;
15730 }
15731
15732 #[tokio::test(flavor = "multi_thread")]
15733 async fn test_extra_newlines_add_constraint() {
15734 let initial_source = "@settings(defaultLengthUnit = mm)
15736
15737sketch001 = sketch(on = XY) {
15738 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15739 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15740}
15741";
15742
15743 let program = Program::parse(initial_source).unwrap().0.unwrap();
15744 let mut frontend = FrontendState::new();
15745
15746 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15747 let mock_ctx = ExecutorContext::new_mock(None).await;
15748 let version = Version(0);
15749 let project_id = ProjectId(0);
15750 let file_id = FileId(0);
15751
15752 frontend.hack_set_program(&ctx, program).await.unwrap();
15753 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15754 let sketch_id = sketch_object.id;
15755 let sketch = expect_sketch(sketch_object);
15756
15757 let line_ids: Vec<ObjectId> = sketch
15759 .segments
15760 .iter()
15761 .copied()
15762 .filter(|seg_id| {
15763 matches!(
15764 &frontend.scene_graph.objects[seg_id.0].kind,
15765 ObjectKind::Segment {
15766 segment: Segment::Line(_)
15767 }
15768 )
15769 })
15770 .collect();
15771 assert_eq!(line_ids.len(), 2, "Expected two line segments");
15772
15773 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15774 let ObjectKind::Segment {
15775 segment: Segment::Line(line1_data),
15776 } = &line1.kind
15777 else {
15778 panic!("Expected line");
15779 };
15780 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15781 let ObjectKind::Segment {
15782 segment: Segment::Line(line2_data),
15783 } = &line2.kind
15784 else {
15785 panic!("Expected line");
15786 };
15787
15788 let constraint = Constraint::Coincident(Coincident {
15790 segments: vec![line1_data.end.into(), line2_data.start.into()],
15791 });
15792
15793 frontend
15795 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15796 .await
15797 .unwrap();
15798 let (src_delta, _scene_delta) = frontend
15799 .add_constraint(&mock_ctx, version, sketch_id, constraint)
15800 .await
15801 .unwrap();
15802 assert!(
15803 src_delta.text.contains("coincident("),
15804 "Expected coincident constraint in source, got: {}",
15805 src_delta.text
15806 );
15807
15808 ctx.close().await;
15809 mock_ctx.close().await;
15810 }
15811
15812 #[tokio::test(flavor = "multi_thread")]
15813 async fn test_extra_newlines_add_line_then_edit_line() {
15814 let initial_source = "@settings(defaultLengthUnit = mm)
15816
15817sketch001 = sketch(on = XY) {
15818}
15819";
15820
15821 let program = Program::parse(initial_source).unwrap().0.unwrap();
15822 let mut frontend = FrontendState::new();
15823
15824 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
15825 let mock_ctx = ExecutorContext::new_mock(None).await;
15826 let version = Version(0);
15827
15828 frontend.hack_set_program(&ctx, program).await.unwrap();
15829 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15830 let sketch_id = sketch_object.id;
15831
15832 let line_ctor = LineCtor {
15834 start: Point2d {
15835 x: Expr::Number(Number {
15836 value: 0.0,
15837 units: NumericSuffix::Mm,
15838 }),
15839 y: Expr::Number(Number {
15840 value: 0.0,
15841 units: NumericSuffix::Mm,
15842 }),
15843 },
15844 end: Point2d {
15845 x: Expr::Number(Number {
15846 value: 10.0,
15847 units: NumericSuffix::Mm,
15848 }),
15849 y: Expr::Number(Number {
15850 value: 10.0,
15851 units: NumericSuffix::Mm,
15852 }),
15853 },
15854 construction: None,
15855 };
15856 let segment = SegmentCtor::Line(line_ctor);
15857 let (src_delta, scene_delta) = frontend
15858 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15859 .await
15860 .unwrap();
15861 assert!(
15862 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15863 "Expected line in source after add, got: {}",
15864 src_delta.text
15865 );
15866 let line_id = *scene_delta.new_objects.last().unwrap();
15868
15869 let line_ctor = LineCtor {
15871 start: Point2d {
15872 x: Expr::Number(Number {
15873 value: 1.0,
15874 units: NumericSuffix::Mm,
15875 }),
15876 y: Expr::Number(Number {
15877 value: 2.0,
15878 units: NumericSuffix::Mm,
15879 }),
15880 },
15881 end: Point2d {
15882 x: Expr::Number(Number {
15883 value: 13.0,
15884 units: NumericSuffix::Mm,
15885 }),
15886 y: Expr::Number(Number {
15887 value: 14.0,
15888 units: NumericSuffix::Mm,
15889 }),
15890 },
15891 construction: None,
15892 };
15893 let segments = vec![ExistingSegmentCtor {
15894 id: line_id,
15895 ctor: SegmentCtor::Line(line_ctor),
15896 }];
15897 let (src_delta, scene_delta) = frontend
15898 .edit_segments(&mock_ctx, version, sketch_id, segments)
15899 .await
15900 .unwrap();
15901 assert!(
15902 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15903 "Expected edited line in source, got: {}",
15904 src_delta.text
15905 );
15906 assert_eq!(scene_delta.new_objects, vec![]);
15907
15908 ctx.close().await;
15909 mock_ctx.close().await;
15910 }
15911
15912 #[test]
15913 fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15914 let code = "\
15923foo1 = 1
15924sk = sketch() {
15925 p = var 1.5
15926}
159277 + 8
15928";
15929 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15930 let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15931 panic!("expected an expression statement");
15932 };
15933 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15934 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15935 &mut ast,
15936 &source_ref,
15937 AstMutateCommand::AddVariableDeclaration {
15938 prefix: "foo".to_owned(),
15939 },
15940 )
15941 .unwrap();
15942 let AstMutateCommandReturn::Name(name) = cmd_return else {
15943 panic!("expected a generated name");
15944 };
15945 assert_eq!(name, "foo2");
15946 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15947 panic!("expected the expression statement to become a variable declaration");
15948 };
15949 assert_eq!(decl.name(), "foo2");
15950 }
15951
15952 fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15954 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15955 panic!("expected a variable declaration");
15956 };
15957 let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15958 panic!("expected a function expression");
15959 };
15960 &func.body
15961 }
15962
15963 fn then_block_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15966 let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15967 panic!("expected a variable declaration");
15968 };
15969 let ast::Expr::IfExpression(if_expr) = &decl.declaration.init else {
15970 panic!("expected an if expression");
15971 };
15972 &if_expr.then_val
15973 }
15974
15975 #[test]
15976 fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15977 let code = "\
15982fn build() {
15983 thing1 = 1
15984 10 + 20
15985 return thing1
15986}
15987";
15988 let mut ast = crate::parsing::top_level_parse(code).unwrap();
15989 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15990 panic!("expected an expression statement");
15991 };
15992 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15993 let (_, cmd_return) = mutate_ast_node_by_source_ref(
15994 &mut ast,
15995 &source_ref,
15996 AstMutateCommand::AddVariableDeclaration {
15997 prefix: "thing".to_owned(),
15998 },
15999 )
16000 .unwrap();
16001 let AstMutateCommandReturn::Name(name) = cmd_return else {
16002 panic!("expected a generated name");
16003 };
16004 assert_eq!(name, "thing2");
16005 let body = &function_body_at(&ast, 0).body;
16006 assert_eq!(body.len(), 3);
16007 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16008 panic!("expected the expression statement to become a variable declaration");
16009 };
16010 assert_eq!(decl.name(), "thing2");
16011 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16013 panic!("expected a variable declaration");
16014 };
16015 assert_eq!(first.name(), "thing1");
16016 assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
16017 }
16018
16019 #[test]
16020 fn test_delete_node_in_function_body_preserves_leading_comment() {
16021 let code = "\
16025fn build() {
16026 a = 1
16027 // keep me
16028 b = 2
16029 return a
16030}
16031";
16032 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16033 let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
16034 panic!("expected a variable declaration");
16035 };
16036 assert_eq!(b_decl.name(), "b");
16037 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16038 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16039 let body = &function_body_at(&ast, 0).body;
16040 assert_eq!(body.len(), 2, "expected b to be deleted");
16041 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16042 panic!("expected a variable declaration");
16043 };
16044 assert_eq!(first.name(), "a");
16045 let ast::BodyItem::ReturnStatement(_) = &body[1] else {
16046 panic!("expected the return statement to remain");
16047 };
16048 assert!(
16049 body[1].get_comments().iter().any(|c| c.contains("keep me")),
16050 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16051 body[1].get_comments()
16052 );
16053 }
16054
16055 #[test]
16056 fn test_add_variable_declaration_in_function_body_ignores_parameters() {
16057 let code = "\
16065fn build(thing2) {
16066 thing1 = 1
16067 10 + 20
16068 return thing1 + thing2
16069}
16070";
16071 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16072 let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16073 panic!("expected an expression statement");
16074 };
16075 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16076 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16077 &mut ast,
16078 &source_ref,
16079 AstMutateCommand::AddVariableDeclaration {
16080 prefix: "thing".to_owned(),
16081 },
16082 )
16083 .unwrap();
16084 let AstMutateCommandReturn::Name(name) = cmd_return else {
16085 panic!("expected a generated name");
16086 };
16087 assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16088 }
16089
16090 #[test]
16091 fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16092 let code = "\
16093x = 1
16094y = if x > 0 {
16095 q1 = 1
16096 foo(q1)
16097 q1
16098} else {
16099 2
16100}
16101";
16102 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16103 let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16104 panic!("expected an expression statement");
16105 };
16106 let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16107 let (_, cmd_return) = mutate_ast_node_by_source_ref(
16108 &mut ast,
16109 &source_ref,
16110 AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16111 )
16112 .unwrap();
16113 let AstMutateCommandReturn::Name(name) = cmd_return else {
16114 panic!("expected a generated name");
16115 };
16116 assert_eq!(name, "q2");
16117 let body = &then_block_at(&ast, 1).body;
16118 assert_eq!(body.len(), 3);
16119 let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16120 panic!("expected the expression statement to become a variable declaration");
16121 };
16122 assert_eq!(decl.name(), "q2");
16123 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16125 panic!("expected a variable declaration");
16126 };
16127 assert_eq!(first.name(), "q1");
16128 assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16129 }
16130
16131 #[test]
16132 fn test_delete_node_in_if_branch_preserves_leading_comment() {
16133 let code = "\
16136y = if true {
16137 a = 1
16138 // keep me
16139 b = 2
16140 a + b
16141} else {
16142 2
16143}
16144";
16145 let mut ast = crate::parsing::top_level_parse(code).unwrap();
16146 let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16147 panic!("expected a variable declaration");
16148 };
16149 assert_eq!(b_decl.name(), "b");
16150 let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16151 mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16152 let body = &then_block_at(&ast, 0).body;
16153 assert_eq!(body.len(), 2, "expected b to be deleted");
16154 let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16155 panic!("expected a variable declaration");
16156 };
16157 assert_eq!(first.name(), "a");
16158 let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16159 panic!("expected the tail expression to remain");
16160 };
16161 assert!(
16162 body[1].get_comments().iter().any(|c| c.contains("keep me")),
16163 "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16164 body[1].get_comments()
16165 );
16166 }
16167}