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_error::CompilationIssue;
9use kcl_error::SourceRange;
10use kittycad_modeling_cmds::units::UnitLength;
11use serde::Serialize;
12
13use crate::ExecOutcome;
14use crate::ExecutorContext;
15use crate::KclError;
16use crate::KclErrorWithOutputs;
17use crate::Program;
18use crate::SegmentDragAnchor;
19use crate::collections::AhashIndexSet;
20use crate::execution::Artifact;
21use crate::execution::ArtifactGraph;
22use crate::execution::CapSubType;
23use crate::execution::MockConfig;
24use crate::execution::SKETCH_BLOCK_PARAM_ON;
25use crate::execution::annotations::WarningLevel;
26use crate::execution::cache::SketchModeState;
27use crate::execution::cache::clear_mem_cache;
28use crate::execution::cache::read_old_memory;
29use crate::execution::cache::write_old_memory;
30use crate::execution::types::adjust_length;
31use crate::fmt::format_number_literal;
32use crate::front::Angle;
33use crate::front::ArcCtor;
34use crate::front::CircleCtor;
35use crate::front::ControlPointSplineCtor;
36use crate::front::Distance;
37use crate::front::EqualRadius;
38use crate::front::Error;
39use crate::front::ExecResult;
40use crate::front::FixedPoint;
41use crate::front::Freedom;
42use crate::front::LinesEqualLength;
43use crate::front::Midpoint;
44use crate::front::Object;
45use crate::front::Parallel;
46use crate::front::Perpendicular;
47use crate::front::PointCtor;
48use crate::front::Symmetric;
49use crate::front::Tangent;
50use crate::frontend::api::Expr;
51use crate::frontend::api::FileId;
52use crate::frontend::api::Number;
53use crate::frontend::api::ObjectId;
54use crate::frontend::api::ObjectKind;
55use crate::frontend::api::Plane;
56use crate::frontend::api::ProjectId;
57use crate::frontend::api::RestoreSketchCheckpointOutcome;
58use crate::frontend::api::SceneGraph;
59use crate::frontend::api::SceneGraphDelta;
60use crate::frontend::api::SketchCheckpointId;
61use crate::frontend::api::SourceDelta;
62use crate::frontend::api::SourceRef;
63use crate::frontend::api::Version;
64use crate::frontend::modify::find_defined_names;
65use crate::frontend::modify::next_free_name;
66use crate::frontend::modify::next_free_name_with_padding;
67use crate::frontend::sketch::Coincident;
68use crate::frontend::sketch::Constraint;
69use crate::frontend::sketch::ConstraintSegment;
70use crate::frontend::sketch::Diameter;
71use crate::frontend::sketch::ExistingSegmentCtor;
72use crate::frontend::sketch::Horizontal;
73use crate::frontend::sketch::LineCtor;
74use crate::frontend::sketch::Point2d;
75use crate::frontend::sketch::Radius;
76use crate::frontend::sketch::Segment;
77use crate::frontend::sketch::SegmentCtor;
78use crate::frontend::sketch::SketchApi;
79use crate::frontend::sketch::SketchCtor;
80use crate::frontend::sketch::Vertical;
81use crate::frontend::traverse::MutateBodyItem;
82use crate::frontend::traverse::TraversalReturn;
83use crate::frontend::traverse::Visitor;
84use crate::frontend::traverse::dfs_mut;
85use crate::id::IncIdGenerator;
86use crate::parsing::ast::types as ast;
87use crate::pretty::NumericSuffix;
88use crate::std::constraints::LinesAtAngleKind;
89use crate::walk::NodeMut;
90use crate::walk::Visitable;
91
92pub(crate) mod api;
93pub(crate) mod modify;
94pub(crate) mod sketch;
95
96pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
97
98#[derive(Debug, Clone)]
99struct SketchCheckpoint {
100 id: SketchCheckpointId,
101 source: SourceDelta,
102 program: Program,
103 scene_graph: SceneGraph,
104 exec_outcome: ExecOutcome,
105 point_freedom_cache: HashMap<ObjectId, Freedom>,
106 mock_memory: Option<SketchModeState>,
107}
108mod traverse;
109pub(crate) mod trim;
110
111struct ArcSizeConstraintParams {
112 points: Vec<ObjectId>,
113 function_name: &'static str,
114 value: f64,
115 units: NumericSuffix,
116 label_position: Option<Point2d<Number>>,
117 constraint_type_name: &'static str,
118}
119
120const POINT_FN: &str = "point";
121const POINT_AT_PARAM: &str = "at";
122const LINE_FN: &str = "line";
123const LINE_VARIABLE: &str = "line";
124const LINE_START_PARAM: &str = "start";
125const LINE_END_PARAM: &str = "end";
126const ARC_FN: &str = "arc";
127const ARC_VARIABLE: &str = "arc";
128const ARC_START_PARAM: &str = "start";
129const ARC_END_PARAM: &str = "end";
130const ARC_CENTER_PARAM: &str = "center";
131const CIRCLE_FN: &str = "circle";
132const CIRCLE_VARIABLE: &str = "circle";
133const CIRCLE_START_PARAM: &str = "start";
134const CIRCLE_CENTER_PARAM: &str = "center";
135const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
136const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
137const LABEL_POSITION_PARAM: &str = "labelPosition";
138
139const COINCIDENT_FN: &str = "coincident";
140const DIAMETER_FN: &str = "diameter";
141const DISTANCE_FN: &str = "distance";
142const FIXED_FN: &str = "fixed";
143const ANGLE_FN: &str = "angle";
144const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
145const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
146const EQUAL_LENGTH_FN: &str = "equalLength";
147const EQUAL_RADIUS_FN: &str = "equalRadius";
148const HORIZONTAL_FN: &str = "horizontal";
149const MIDPOINT_FN: &str = "midpoint";
150const MIDPOINT_POINT_PARAM: &str = "point";
151const RADIUS_FN: &str = "radius";
152const SYMMETRIC_FN: &str = "symmetric";
153const SYMMETRIC_AXIS_PARAM: &str = "axis";
154const TANGENT_FN: &str = "tangent";
155const VERTICAL_FN: &str = "vertical";
156
157const LINE_PROPERTY_START: &str = "start";
158const LINE_PROPERTY_END: &str = "end";
159
160const ARC_PROPERTY_START: &str = "start";
161const ARC_PROPERTY_END: &str = "end";
162const ARC_PROPERTY_CENTER: &str = "center";
163const CIRCLE_PROPERTY_START: &str = "start";
164const CIRCLE_PROPERTY_CENTER: &str = "center";
165const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
166const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
167
168const CONSTRUCTION_PARAM: &str = "construction";
169
170#[derive(Debug, Clone, Copy)]
171enum EditDeleteKind {
172 Edit,
173 DeleteNonSketch,
174}
175
176struct ExecuteAfterEditOptions {
178 segment_ids_edited: AhashIndexSet<ObjectId>,
179 edit_kind: EditDeleteKind,
180 commit_solved_initial_guesses: bool,
181}
182
183impl EditDeleteKind {
184 fn is_delete(&self) -> bool {
186 match self {
187 EditDeleteKind::Edit => false,
188 EditDeleteKind::DeleteNonSketch => true,
189 }
190 }
191
192 fn to_change_kind(self) -> ChangeKind {
193 match self {
194 EditDeleteKind::Edit => ChangeKind::Edit,
195 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
196 }
197 }
198}
199
200#[derive(Debug, Clone, Copy)]
201enum ChangeKind {
202 Add,
203 Edit,
204 Delete,
205 None,
206}
207
208#[derive(Debug, Clone, Serialize, ts_rs::TS)]
209#[ts(export, export_to = "FrontendApi.ts")]
210#[serde(tag = "type")]
211pub enum SetProgramOutcome {
212 #[serde(rename_all = "camelCase")]
213 Success {
214 scene_graph: Box<SceneGraph>,
215 exec_outcome: Box<ExecOutcome>,
216 checkpoint_id: Option<SketchCheckpointId>,
217 },
218 #[serde(rename_all = "camelCase")]
219 ExecFailure { error: Box<KclErrorWithOutputs> },
220}
221
222pub struct EditSegmentsOptions {
224 pub anchor_segment_ids: Option<Vec<ObjectId>>,
230 pub drag_anchors: Vec<SegmentDragAnchor>,
233 pub commit_solved_initial_guesses: bool,
235}
236
237pub struct EditDistanceConstraintLabelPositionOptions {
239 pub anchor_segment_ids: Vec<ObjectId>,
241 pub commit_solved_initial_guesses: bool,
243}
244
245#[derive(Debug, Clone)]
246pub struct FrontendState {
247 program: Program,
248 scene_graph: SceneGraph,
249 point_freedom_cache: HashMap<ObjectId, Freedom>,
252 next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
255 next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
258 next_edit_commits_solver_solutions: Option<bool>,
262 sketch_checkpoints: VecDeque<SketchCheckpoint>,
263 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
264}
265
266impl Default for FrontendState {
267 fn default() -> Self {
268 Self::new()
269 }
270}
271
272impl FrontendState {
273 pub fn new() -> Self {
274 Self {
275 program: Program::empty(),
276 scene_graph: SceneGraph {
277 project: ProjectId(0),
278 file: FileId(0),
279 version: Version(0),
280 objects: Default::default(),
281 settings: Default::default(),
282 sketch_mode: Default::default(),
283 },
284 point_freedom_cache: HashMap::new(),
285 next_drag_anchor_segment_ids: None,
286 next_segment_drag_anchors: None,
287 next_edit_commits_solver_solutions: None,
288 sketch_checkpoints: VecDeque::new(),
289 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
290 }
291 }
292
293 pub fn scene_graph(&self) -> &SceneGraph {
295 &self.scene_graph
296 }
297
298 pub fn default_length_unit(&self) -> UnitLength {
299 self.program
300 .meta_settings()
301 .ok()
302 .flatten()
303 .map(|settings| settings.default_length_units)
304 .unwrap_or(UnitLength::Millimeters)
305 }
306
307 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
308 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
309
310 let checkpoint = SketchCheckpoint {
311 id: checkpoint_id,
312 source: SourceDelta {
313 text: source_from_ast(&self.program.ast),
314 },
315 program: self.program.clone(),
316 scene_graph: self.scene_graph.clone(),
317 exec_outcome,
318 point_freedom_cache: self.point_freedom_cache.clone(),
319 mock_memory: read_old_memory().await,
320 };
321
322 self.sketch_checkpoints.push_back(checkpoint);
323 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
324 self.sketch_checkpoints.pop_front();
325 }
326
327 Ok(checkpoint_id)
328 }
329
330 pub async fn edit_segments_with_options(
338 &mut self,
339 ctx: &ExecutorContext,
340 version: Version,
341 sketch: ObjectId,
342 segments: Vec<ExistingSegmentCtor>,
343 options: EditSegmentsOptions,
344 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
345 let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
346 self.next_drag_anchor_segment_ids
347 .replace(anchor_ids.into_iter().collect())
348 });
349 let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
350 let previous_commit_mode = self
351 .next_edit_commits_solver_solutions
352 .replace(options.commit_solved_initial_guesses);
353 let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
354 if let Some(previous_anchor_ids) = previous_anchor_ids {
355 self.next_drag_anchor_segment_ids = previous_anchor_ids;
356 }
357 self.next_segment_drag_anchors = previous_drag_anchors;
358 self.next_edit_commits_solver_solutions = previous_commit_mode;
359 result
360 }
361
362 pub async fn edit_distance_constraint_label_position_with_options(
368 &mut self,
369 ctx: &ExecutorContext,
370 version: Version,
371 sketch: ObjectId,
372 constraint_id: ObjectId,
373 label_position: Point2d<Number>,
374 options: EditDistanceConstraintLabelPositionOptions,
375 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
376 let previous_commit_mode = self
377 .next_edit_commits_solver_solutions
378 .replace(options.commit_solved_initial_guesses);
379 let result = SketchApi::edit_distance_constraint_label_position(
380 self,
381 ctx,
382 version,
383 sketch,
384 constraint_id,
385 label_position,
386 options.anchor_segment_ids,
387 )
388 .await;
389 self.next_edit_commits_solver_solutions = previous_commit_mode;
390 result
391 }
392
393 pub async fn restore_sketch_checkpoint(
394 &mut self,
395 checkpoint_id: SketchCheckpointId,
396 ) -> api::Result<RestoreSketchCheckpointOutcome> {
397 let checkpoint = self
398 .sketch_checkpoints
399 .iter()
400 .find(|checkpoint| checkpoint.id == checkpoint_id)
401 .cloned()
402 .ok_or_else(|| Error {
403 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
404 })?;
405
406 self.program = checkpoint.program;
407 self.scene_graph = checkpoint.scene_graph.clone();
408 self.point_freedom_cache = checkpoint.point_freedom_cache;
409 self.next_drag_anchor_segment_ids = None;
410 self.next_segment_drag_anchors = None;
411 self.next_edit_commits_solver_solutions = None;
412
413 if let Some(mock_memory) = checkpoint.mock_memory {
414 write_old_memory(mock_memory).await;
415 } else {
416 clear_mem_cache().await;
417 }
418
419 Ok(RestoreSketchCheckpointOutcome {
420 source_delta: checkpoint.source,
421 scene_graph_delta: SceneGraphDelta {
422 new_graph: self.scene_graph_for_ui(),
423 new_objects: Vec::new(),
424 invalidates_ids: true,
425 exec_outcome: checkpoint.exec_outcome,
426 },
427 })
428 }
429
430 pub fn clear_sketch_checkpoints(&mut self) {
431 self.sketch_checkpoints.clear();
432 }
433 fn scene_graph_for_ui(&self) -> SceneGraph {
434 let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
435 matches!(
436 object.kind,
437 ObjectKind::Segment {
438 segment: Segment::ControlPointSpline(_)
439 }
440 )
441 });
442
443 if !has_control_point_splines {
444 return self.scene_graph.clone();
445 }
446
447 let hidden_constraint_ids = self
448 .scene_graph
449 .objects
450 .iter()
451 .filter_map(|object| match &object.kind {
452 ObjectKind::Constraint {
453 constraint: Constraint::Coincident(coincident),
454 } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
455 Some(object.id)
456 }
457 _ => None,
458 })
459 .collect::<HashSet<_>>();
460
461 if hidden_constraint_ids.is_empty() {
462 return self.scene_graph.clone();
463 }
464
465 let mut scene_graph = self.scene_graph.clone();
466 for object in &mut scene_graph.objects {
467 match &mut object.kind {
468 ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
469 object.kind = ObjectKind::Nil;
470 }
471 ObjectKind::Sketch(sketch) => {
472 sketch
473 .constraints
474 .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
475 }
476 _ => {}
477 }
478 }
479
480 scene_graph
481 }
482}
483
484fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
485 let mut first_owner_id = None;
486 for segment_id in coincident.segment_ids() {
487 let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
488 return false;
489 };
490
491 match first_owner_id {
492 Some(first_owner_id) if first_owner_id != owner_id => return false,
493 Some(_) => {}
494 None => first_owner_id = Some(owner_id),
495 }
496 }
497
498 first_owner_id.is_some()
499}
500
501fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
502 let object = scene_graph.objects.get(segment_id.0)?;
503 let ObjectKind::Segment { segment } = &object.kind else {
504 return None;
505 };
506
507 match segment {
508 Segment::ControlPointSpline(_) => Some(segment_id),
509 Segment::Point(point) => point
510 .owner
511 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
512 Segment::Line(line) => line
513 .owner
514 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
515 _ => None,
516 }
517}
518
519fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
520 matches!(
521 scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
522 Some(ObjectKind::Segment {
523 segment: Segment::ControlPointSpline(_)
524 })
525 )
526}
527
528fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
529 let experimental_features_allowed = program
530 .meta_settings()
531 .ok()
532 .flatten()
533 .map(|settings| settings.experimental_features == WarningLevel::Allow)
534 .unwrap_or(false);
535 if experimental_features_allowed {
536 return Ok(program.clone());
537 }
538
539 program.change_experimental_features(Some(WarningLevel::Allow))
540}
541
542impl SketchApi for FrontendState {
543 async fn execute_mock(
544 &mut self,
545 ctx: &ExecutorContext,
546 _version: Version,
547 sketch: ObjectId,
548 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
549 let sketch_block_ref =
550 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
551
552 let mut truncated_program = self.program.clone();
553 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
554 .map_err(KclErrorWithOutputs::no_outputs)?;
555
556 let outcome = ctx
558 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
559 .await?;
560 let new_source = source_from_ast(&self.program.ast);
561 let src_delta = SourceDelta { text: new_source };
562 let outcome = self.update_state_after_exec(outcome, true);
564 let scene_graph_delta = SceneGraphDelta {
565 new_graph: self.scene_graph.clone(),
566 new_objects: Default::default(),
567 invalidates_ids: false,
568 exec_outcome: outcome,
569 };
570 Ok((src_delta, scene_graph_delta))
571 }
572
573 async fn new_sketch(
574 &mut self,
575 ctx: &ExecutorContext,
576 _project: ProjectId,
577 _file: FileId,
578 _version: Version,
579 args: SketchCtor,
580 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
581 let mut new_ast = self.program.ast.clone();
584 let mut plane_ast =
586 sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
587 let mut defined_names = find_defined_names(&new_ast);
588 let is_face_of_expr = matches!(
589 &plane_ast,
590 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
591 );
592 if is_face_of_expr {
593 let face_name = next_free_name_with_padding("face", &defined_names)
594 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
595 let face_decl = ast::VariableDeclaration::new(
596 ast::VariableDeclarator::new(&face_name, plane_ast),
597 ast::ItemVisibility::Default,
598 ast::VariableKind::Const,
599 );
600 new_ast
601 .body
602 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
603 face_decl,
604 ))));
605 defined_names.insert(face_name.clone());
606 plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
607 }
608 let sketch_ast = ast::SketchBlock {
609 arguments: vec![ast::LabeledArg {
610 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
611 arg: plane_ast,
612 }],
613 body: Default::default(),
614 is_being_edited: false,
615 non_code_meta: Default::default(),
616 digest: None,
617 };
618 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
621 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
622 let sketch_decl = ast::VariableDeclaration::new(
623 ast::VariableDeclarator::new(
624 &sketch_name,
625 ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
626 ),
627 ast::ItemVisibility::Default,
628 ast::VariableKind::Const,
629 );
630 new_ast
631 .body
632 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
633 sketch_decl,
634 ))));
635 let new_source = source_from_ast(&new_ast);
637 let (new_program, errors) = Program::parse(&new_source)
639 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
640 if !errors.is_empty() {
641 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
642 "Error parsing KCL source after adding sketch: {errors:?}"
643 ))));
644 }
645 let Some(new_program) = new_program else {
646 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
647 "No AST produced after adding sketch".to_owned(),
648 )));
649 };
650
651 self.program = new_program.clone();
653
654 let outcome = ctx.run_with_caching(new_program.clone()).await?;
657 let freedom_analysis_ran = true;
658
659 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
660
661 let Some(sketch_id) = self
662 .scene_graph
663 .objects
664 .iter()
665 .filter_map(|object| match object.kind {
666 ObjectKind::Sketch(_) => Some(object.id),
667 _ => None,
668 })
669 .max_by_key(|id| id.0)
670 else {
671 return Err(KclErrorWithOutputs::from_error_outcome(
672 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
673 outcome,
674 ));
675 };
676 self.scene_graph.sketch_mode = Some(sketch_id);
678
679 let src_delta = SourceDelta { text: new_source };
680 let scene_graph_delta = SceneGraphDelta {
681 new_graph: self.scene_graph_for_ui(),
682 invalidates_ids: false,
683 new_objects: vec![sketch_id],
684 exec_outcome: outcome,
685 };
686 Ok((src_delta, scene_graph_delta, sketch_id))
687 }
688
689 async fn edit_sketch(
690 &mut self,
691 ctx: &ExecutorContext,
692 _project: ProjectId,
693 _file: FileId,
694 _version: Version,
695 sketch: ObjectId,
696 ) -> ExecResult<SceneGraphDelta> {
697 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
701 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
702 })?;
703 let ObjectKind::Sketch(_) = &sketch_object.kind else {
704 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
705 "Object is not a sketch, it is {}",
706 sketch_object.kind.human_friendly_kind_with_article()
707 ))));
708 };
709 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
710
711 self.scene_graph.sketch_mode = Some(sketch);
713
714 let mut truncated_program = self.program.clone();
716 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
717 .map_err(KclErrorWithOutputs::no_outputs)?;
718
719 let outcome = ctx
722 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
723 .await?;
724
725 let outcome = self.update_state_after_exec(outcome, true);
727 let scene_graph_delta = SceneGraphDelta {
728 new_graph: self.scene_graph_for_ui(),
729 invalidates_ids: false,
730 new_objects: Vec::new(),
731 exec_outcome: outcome,
732 };
733 Ok(scene_graph_delta)
734 }
735
736 async fn exit_sketch(
737 &mut self,
738 ctx: &ExecutorContext,
739 _version: Version,
740 sketch: ObjectId,
741 ) -> ExecResult<SceneGraph> {
742 #[cfg(not(target_arch = "wasm32"))]
744 let _ = sketch;
745 #[cfg(target_arch = "wasm32")]
746 if self.scene_graph.sketch_mode != Some(sketch) {
747 web_sys::console::warn_1(
748 &format!(
749 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
750 &self.scene_graph.sketch_mode
751 )
752 .into(),
753 );
754 }
755 self.scene_graph.sketch_mode = None;
756
757 let outcome = ctx.run_with_caching(self.program.clone()).await?;
759
760 self.update_state_after_exec(outcome, false);
762
763 Ok(self.scene_graph_for_ui())
764 }
765
766 async fn delete_sketch(
767 &mut self,
768 ctx: &ExecutorContext,
769 _version: Version,
770 sketch: ObjectId,
771 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
772 let mut new_ast = self.program.ast.clone();
775
776 let sketch_id = sketch;
778 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
779 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
780 })?;
781 let ObjectKind::Sketch(_) = &sketch_object.kind else {
782 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
783 "Object is not a sketch, it is {}",
784 sketch_object.kind.human_friendly_kind_with_article(),
785 ))));
786 };
787
788 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
790 .map_err(KclErrorWithOutputs::no_outputs)?;
791
792 self.execute_after_delete_sketch(ctx, &mut new_ast).await
793 }
794
795 async fn add_segment(
796 &mut self,
797 ctx: &ExecutorContext,
798 _version: Version,
799 sketch: ObjectId,
800 segment: SegmentCtor,
801 _label: Option<String>,
802 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
803 match segment {
805 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
806 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
807 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
808 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
809 SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
810 }
811 }
812
813 async fn edit_segments(
814 &mut self,
815 ctx: &ExecutorContext,
816 _version: Version,
817 sketch: ObjectId,
818 segments: Vec<ExistingSegmentCtor>,
819 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
820 let sketch_block_ref =
822 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
823
824 let mut new_ast = self.program.ast.clone();
825 let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
826 let mut invalidates_ids = false;
827
828 for segment in &segments {
831 edited_segment_ids.insert(segment.id);
832 if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
833 && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
834 && let ObjectKind::Segment {
835 segment: Segment::ControlPointSpline(existing_spline),
836 } = &existing_object.kind
837 && existing_spline.controls.len() != new_ctor.points.len()
838 {
839 invalidates_ids = true;
840 }
841 }
842 let drag_anchor_segment_ids = self
843 .next_drag_anchor_segment_ids
844 .take()
845 .unwrap_or_else(|| edited_segment_ids.clone());
846 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
847
848 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
863
864 for segment in segments {
865 let segment_id = segment.id;
866 match segment.ctor {
867 SegmentCtor::Point(ctor) => {
868 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
870 && let ObjectKind::Segment { segment } = &segment_object.kind
871 && let Segment::Point(point) = segment
872 && let Some(owner_id) = point.owner
873 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
874 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
875 {
876 match owner_segment {
877 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
878 if let Some(existing) = final_edits.get_mut(&owner_id) {
879 let SegmentCtor::Line(line_ctor) = existing else {
880 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
881 "Internal: Expected line ctor for owner, but found {}",
882 existing.human_friendly_kind_with_article()
883 ))));
884 };
885 if line.start == segment_id {
887 line_ctor.start = ctor.position;
888 } else {
889 line_ctor.end = ctor.position;
890 }
891 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
892 let mut line_ctor = line_ctor.clone();
894 if line.start == segment_id {
895 line_ctor.start = ctor.position;
896 } else {
897 line_ctor.end = ctor.position;
898 }
899 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
900 } else {
901 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
903 "Internal: Line does not have line ctor, but found {}",
904 line.ctor.human_friendly_kind_with_article()
905 ))));
906 }
907 continue;
908 }
909 Segment::Arc(arc)
910 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
911 {
912 if let Some(existing) = final_edits.get_mut(&owner_id) {
913 let SegmentCtor::Arc(arc_ctor) = existing else {
914 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
915 "Internal: Expected arc ctor for owner, but found {}",
916 existing.human_friendly_kind_with_article()
917 ))));
918 };
919 if arc.start == segment_id {
920 arc_ctor.start = ctor.position;
921 } else if arc.end == segment_id {
922 arc_ctor.end = ctor.position;
923 } else {
924 arc_ctor.center = ctor.position;
925 }
926 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
927 let mut arc_ctor = arc_ctor.clone();
928 if arc.start == segment_id {
929 arc_ctor.start = ctor.position;
930 } else if arc.end == segment_id {
931 arc_ctor.end = ctor.position;
932 } else {
933 arc_ctor.center = ctor.position;
934 }
935 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
936 } else {
937 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
938 "Internal: Arc does not have arc ctor, but found {}",
939 arc.ctor.human_friendly_kind_with_article()
940 ))));
941 }
942 continue;
943 }
944 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
945 if let Some(existing) = final_edits.get_mut(&owner_id) {
946 let SegmentCtor::Circle(circle_ctor) = existing else {
947 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
948 "Internal: Expected circle ctor for owner, but found {}",
949 existing.human_friendly_kind_with_article()
950 ))));
951 };
952 if circle.start == segment_id {
953 circle_ctor.start = ctor.position;
954 } else {
955 circle_ctor.center = ctor.position;
956 }
957 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
958 let mut circle_ctor = circle_ctor.clone();
959 if circle.start == segment_id {
960 circle_ctor.start = ctor.position;
961 } else {
962 circle_ctor.center = ctor.position;
963 }
964 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
965 } else {
966 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
967 "Internal: Circle does not have circle ctor, but found {}",
968 circle.ctor.human_friendly_kind_with_article()
969 ))));
970 }
971 continue;
972 }
973 Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
974 let Some(control_index) =
975 spline.controls.iter().position(|control_id| *control_id == segment_id)
976 else {
977 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
978 "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
979 ))));
980 };
981 if let Some(existing) = final_edits.get_mut(&owner_id) {
982 let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
983 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
984 "Internal: Expected controlPointSpline ctor for owner, but found {}",
985 existing.human_friendly_kind_with_article()
986 ))));
987 };
988 spline_ctor.points[control_index] = ctor.position;
989 } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
990 let mut spline_ctor = spline_ctor.clone();
991 spline_ctor.points[control_index] = ctor.position;
992 final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
993 } else {
994 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
995 "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
996 spline.ctor.human_friendly_kind_with_article()
997 ))));
998 }
999 continue;
1000 }
1001 _ => {}
1002 }
1003 }
1004
1005 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1007 }
1008 SegmentCtor::Line(ctor) => {
1009 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1010 }
1011 SegmentCtor::Arc(ctor) => {
1012 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1013 }
1014 SegmentCtor::Circle(ctor) => {
1015 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1016 }
1017 SegmentCtor::ControlPointSpline(ctor) => {
1018 final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1019 }
1020 }
1021 }
1022
1023 for (segment_id, ctor) in final_edits {
1024 match ctor {
1025 SegmentCtor::Point(ctor) => self
1026 .edit_point(&mut new_ast, sketch, segment_id, ctor)
1027 .map_err(KclErrorWithOutputs::no_outputs)?,
1028 SegmentCtor::Line(ctor) => self
1029 .edit_line(&mut new_ast, sketch, segment_id, ctor)
1030 .map_err(KclErrorWithOutputs::no_outputs)?,
1031 SegmentCtor::Arc(ctor) => self
1032 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1033 .map_err(KclErrorWithOutputs::no_outputs)?,
1034 SegmentCtor::Circle(ctor) => self
1035 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1036 .map_err(KclErrorWithOutputs::no_outputs)?,
1037 SegmentCtor::ControlPointSpline(ctor) => self
1038 .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1039 .map_err(KclErrorWithOutputs::no_outputs)?,
1040 }
1041 }
1042 let (source_delta, mut scene_graph_delta) = self
1043 .execute_after_edit(
1044 ctx,
1045 sketch,
1046 sketch_block_ref,
1047 &mut new_ast,
1048 ExecuteAfterEditOptions {
1049 segment_ids_edited: drag_anchor_segment_ids,
1050 edit_kind: EditDeleteKind::Edit,
1051 commit_solved_initial_guesses,
1052 },
1053 )
1054 .await?;
1055 if invalidates_ids {
1056 scene_graph_delta.invalidates_ids = true;
1057 }
1058 Ok((source_delta, scene_graph_delta))
1059 }
1060
1061 async fn delete_objects(
1062 &mut self,
1063 ctx: &ExecutorContext,
1064 _version: Version,
1065 sketch: ObjectId,
1066 constraint_ids: Vec<ObjectId>,
1067 segment_ids: Vec<ObjectId>,
1068 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1069 let sketch_block_ref =
1071 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1072
1073 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1075 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1076
1077 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1080
1081 for segment_id in segment_ids_set.iter().copied() {
1082 let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1083 let ObjectKind::Segment { segment } = &segment_object.kind else {
1084 return None;
1085 };
1086 match segment {
1087 Segment::Point(point) => point.owner,
1088 Segment::Line(line) => line.owner,
1089 _ => None,
1090 }
1091 });
1092
1093 if let Some(owner_id) = owner_id
1094 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1095 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1096 && matches!(
1097 owner_segment,
1098 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1099 )
1100 {
1101 resolved_segment_ids_to_delete.insert(owner_id);
1103 } else {
1104 resolved_segment_ids_to_delete.insert(segment_id);
1106 }
1107 }
1108 let referenced_constraint_ids = self
1109 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1110 .map_err(KclErrorWithOutputs::no_outputs)?;
1111
1112 let mut new_ast = self.program.ast.clone();
1113
1114 for constraint_id in referenced_constraint_ids {
1115 if constraint_ids_set.contains(&constraint_id) {
1116 continue;
1117 }
1118
1119 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1120 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1121 })?;
1122 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1123 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1124 "Object is not a constraint, it is {}",
1125 constraint_object.kind.human_friendly_kind_with_article()
1126 ))));
1127 };
1128
1129 match constraint {
1130 Constraint::Coincident(coincident) => {
1131 let remaining_segments =
1132 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1133
1134 if remaining_segments.len() >= 2 {
1136 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1137 .map_err(KclErrorWithOutputs::no_outputs)?;
1138 } else {
1139 constraint_ids_set.insert(constraint_id);
1140 }
1141 }
1142 Constraint::EqualRadius(equal_radius) => {
1143 let remaining_input = equal_radius
1144 .input
1145 .iter()
1146 .copied()
1147 .filter(|segment_id| {
1148 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1149 })
1150 .collect::<Vec<_>>();
1151
1152 if remaining_input.len() >= 2 {
1153 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1154 .map_err(KclErrorWithOutputs::no_outputs)?;
1155 } else {
1156 constraint_ids_set.insert(constraint_id);
1157 }
1158 }
1159 Constraint::LinesEqualLength(lines_equal_length) => {
1160 let remaining_lines = lines_equal_length
1161 .lines
1162 .iter()
1163 .copied()
1164 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1165 .collect::<Vec<_>>();
1166
1167 if remaining_lines.len() >= 2 {
1169 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1170 .map_err(KclErrorWithOutputs::no_outputs)?;
1171 } else {
1172 constraint_ids_set.insert(constraint_id);
1173 }
1174 }
1175 Constraint::Parallel(parallel) => {
1176 let remaining_lines = parallel
1177 .lines
1178 .iter()
1179 .copied()
1180 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1181 .collect::<Vec<_>>();
1182
1183 if remaining_lines.len() >= 2 {
1184 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1185 .map_err(KclErrorWithOutputs::no_outputs)?;
1186 } else {
1187 constraint_ids_set.insert(constraint_id);
1188 }
1189 }
1190 Constraint::Horizontal(Horizontal::Points { points }) => {
1191 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1192
1193 if remaining_points.len() >= 2 {
1194 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1195 .map_err(KclErrorWithOutputs::no_outputs)?;
1196 } else {
1197 constraint_ids_set.insert(constraint_id);
1198 }
1199 }
1200 Constraint::Vertical(Vertical::Points { points }) => {
1201 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1202
1203 if remaining_points.len() >= 2 {
1204 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1205 .map_err(KclErrorWithOutputs::no_outputs)?;
1206 } else {
1207 constraint_ids_set.insert(constraint_id);
1208 }
1209 }
1210 Constraint::Fixed(fixed) => {
1211 if fixed.points.iter().any(|fixed_point| {
1212 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1213 }) {
1214 constraint_ids_set.insert(constraint_id);
1215 }
1216 }
1217 _ => {
1218 constraint_ids_set.insert(constraint_id);
1220 }
1221 }
1222 }
1223
1224 for constraint_id in constraint_ids_set {
1225 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1226 .map_err(KclErrorWithOutputs::no_outputs)?;
1227 }
1228 for segment_id in resolved_segment_ids_to_delete {
1229 self.delete_segment(&mut new_ast, sketch, segment_id)
1230 .map_err(KclErrorWithOutputs::no_outputs)?;
1231 }
1232
1233 self.execute_after_edit(
1234 ctx,
1235 sketch,
1236 sketch_block_ref,
1237 &mut new_ast,
1238 ExecuteAfterEditOptions {
1239 segment_ids_edited: Default::default(),
1240 edit_kind: EditDeleteKind::DeleteNonSketch,
1241 commit_solved_initial_guesses: true,
1242 },
1243 )
1244 .await
1245 }
1246
1247 async fn add_constraint(
1248 &mut self,
1249 ctx: &ExecutorContext,
1250 _version: Version,
1251 sketch: ObjectId,
1252 constraint: Constraint,
1253 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1254 let original_program = self.program.clone();
1258 let original_scene_graph = self.scene_graph.clone();
1259
1260 let mut new_ast = self.program.ast.clone();
1261 let sketch_block_ref = match constraint {
1262 Constraint::Coincident(coincident) => self
1263 .add_coincident(sketch, coincident, &mut new_ast)
1264 .await
1265 .map_err(KclErrorWithOutputs::no_outputs)?,
1266 Constraint::Distance(distance) => self
1267 .add_distance(sketch, distance, &mut new_ast)
1268 .await
1269 .map_err(KclErrorWithOutputs::no_outputs)?,
1270 Constraint::EqualRadius(equal_radius) => self
1271 .add_equal_radius(sketch, equal_radius, &mut new_ast)
1272 .await
1273 .map_err(KclErrorWithOutputs::no_outputs)?,
1274 Constraint::Fixed(fixed) => self
1275 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1276 .await
1277 .map_err(KclErrorWithOutputs::no_outputs)?,
1278 Constraint::HorizontalDistance(distance) => self
1279 .add_horizontal_distance(sketch, distance, &mut new_ast)
1280 .await
1281 .map_err(KclErrorWithOutputs::no_outputs)?,
1282 Constraint::VerticalDistance(distance) => self
1283 .add_vertical_distance(sketch, distance, &mut new_ast)
1284 .await
1285 .map_err(KclErrorWithOutputs::no_outputs)?,
1286 Constraint::Horizontal(horizontal) => self
1287 .add_horizontal(sketch, horizontal, &mut new_ast)
1288 .await
1289 .map_err(KclErrorWithOutputs::no_outputs)?,
1290 Constraint::LinesEqualLength(lines_equal_length) => self
1291 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1292 .await
1293 .map_err(KclErrorWithOutputs::no_outputs)?,
1294 Constraint::Midpoint(midpoint) => self
1295 .add_midpoint(sketch, midpoint, &mut new_ast)
1296 .await
1297 .map_err(KclErrorWithOutputs::no_outputs)?,
1298 Constraint::Parallel(parallel) => self
1299 .add_parallel(sketch, parallel, &mut new_ast)
1300 .await
1301 .map_err(KclErrorWithOutputs::no_outputs)?,
1302 Constraint::Perpendicular(perpendicular) => self
1303 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1304 .await
1305 .map_err(KclErrorWithOutputs::no_outputs)?,
1306 Constraint::Radius(radius) => self
1307 .add_radius(sketch, radius, &mut new_ast)
1308 .await
1309 .map_err(KclErrorWithOutputs::no_outputs)?,
1310 Constraint::Diameter(diameter) => self
1311 .add_diameter(sketch, diameter, &mut new_ast)
1312 .await
1313 .map_err(KclErrorWithOutputs::no_outputs)?,
1314 Constraint::Symmetric(symmetric) => self
1315 .add_symmetric(sketch, symmetric, &mut new_ast)
1316 .await
1317 .map_err(KclErrorWithOutputs::no_outputs)?,
1318 Constraint::Vertical(vertical) => self
1319 .add_vertical(sketch, vertical, &mut new_ast)
1320 .await
1321 .map_err(KclErrorWithOutputs::no_outputs)?,
1322 Constraint::Angle(lines_at_angle) => self
1323 .add_angle(sketch, lines_at_angle, &mut new_ast)
1324 .await
1325 .map_err(KclErrorWithOutputs::no_outputs)?,
1326 Constraint::Tangent(tangent) => self
1327 .add_tangent(sketch, tangent, &mut new_ast)
1328 .await
1329 .map_err(KclErrorWithOutputs::no_outputs)?,
1330 };
1331
1332 let result = self
1333 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1334 .await;
1335
1336 if result.is_err() {
1338 self.program = original_program;
1339 self.scene_graph = original_scene_graph;
1340 }
1341
1342 result
1343 }
1344
1345 async fn chain_segment(
1346 &mut self,
1347 ctx: &ExecutorContext,
1348 version: Version,
1349 sketch: ObjectId,
1350 previous_segment_end_point_id: ObjectId,
1351 segment: SegmentCtor,
1352 _label: Option<String>,
1353 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1354 let SegmentCtor::Line(line_ctor) = segment else {
1358 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1359 "chain_segment currently only supports Line segments, got {}",
1360 segment.human_friendly_kind_with_article(),
1361 ))));
1362 };
1363
1364 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1366
1367 let new_line_id = first_scene_delta
1370 .new_objects
1371 .iter()
1372 .find(|&obj_id| {
1373 let obj = self.scene_graph.objects.get(obj_id.0);
1374 if let Some(obj) = obj {
1375 matches!(
1376 &obj.kind,
1377 ObjectKind::Segment {
1378 segment: Segment::Line(_)
1379 }
1380 )
1381 } else {
1382 false
1383 }
1384 })
1385 .ok_or_else(|| {
1386 KclErrorWithOutputs::no_outputs(KclError::refactor(
1387 "Failed to find new line segment in scene graph".to_string(),
1388 ))
1389 })?;
1390
1391 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1392 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1393 "New line object not found: {new_line_id:?}"
1394 )))
1395 })?;
1396
1397 let ObjectKind::Segment {
1398 segment: new_line_segment,
1399 } = &new_line_obj.kind
1400 else {
1401 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1402 "Object is not a segment: {new_line_obj:?}"
1403 ))));
1404 };
1405
1406 let Segment::Line(new_line) = new_line_segment else {
1407 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1408 "Segment is not a line: {new_line_segment:?}"
1409 ))));
1410 };
1411
1412 let new_line_start_point_id = new_line.start;
1413
1414 let coincident = Coincident {
1416 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1417 };
1418
1419 let (final_src_delta, final_scene_delta) = self
1420 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1421 .await?;
1422
1423 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1426 combined_new_objects.extend(final_scene_delta.new_objects);
1427
1428 let scene_graph_delta = SceneGraphDelta {
1429 new_graph: self.scene_graph_for_ui(),
1430 invalidates_ids: false,
1431 new_objects: combined_new_objects,
1432 exec_outcome: final_scene_delta.exec_outcome,
1433 };
1434
1435 Ok((final_src_delta, scene_graph_delta))
1436 }
1437
1438 async fn edit_constraint(
1439 &mut self,
1440 ctx: &ExecutorContext,
1441 _version: Version,
1442 sketch: ObjectId,
1443 constraint_id: ObjectId,
1444 value_expression: String,
1445 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1446 let sketch_block_ref =
1448 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1449
1450 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1451 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1452 })?;
1453 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1454 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1455 "Object is not a constraint: {constraint_id:?}"
1456 ))));
1457 }
1458
1459 let mut new_ast = self.program.ast.clone();
1460
1461 let (parsed, errors) = Program::parse(&value_expression)
1463 .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1464 if !errors.is_empty() {
1465 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1466 "Error parsing value expression: {errors:?}"
1467 ))));
1468 }
1469 let mut parsed = parsed.ok_or_else(|| {
1470 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1471 })?;
1472 if parsed.ast.body.is_empty() {
1473 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1474 "Empty value expression".to_string(),
1475 )));
1476 }
1477 let first = parsed.ast.body.remove(0);
1478 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1479 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1480 "Value expression must be a simple expression".to_string(),
1481 )));
1482 };
1483
1484 let new_value: ast::BinaryPart = expr_stmt
1485 .inner
1486 .expression
1487 .try_into()
1488 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1489
1490 self.mutate_ast(
1491 &mut new_ast,
1492 constraint_id,
1493 AstMutateCommand::EditConstraintValue { value: new_value },
1494 )
1495 .map_err(KclErrorWithOutputs::no_outputs)?;
1496
1497 self.execute_after_edit(
1498 ctx,
1499 sketch,
1500 sketch_block_ref,
1501 &mut new_ast,
1502 ExecuteAfterEditOptions {
1503 segment_ids_edited: Default::default(),
1504 edit_kind: EditDeleteKind::Edit,
1505 commit_solved_initial_guesses: true,
1506 },
1507 )
1508 .await
1509 }
1510
1511 async fn edit_distance_constraint_label_position(
1512 &mut self,
1513 ctx: &ExecutorContext,
1514 _version: Version,
1515 sketch: ObjectId,
1516 constraint_id: ObjectId,
1517 label_position: Point2d<Number>,
1518 anchor_segment_ids: Vec<ObjectId>,
1519 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1520 let sketch_block_ref =
1522 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1523
1524 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1525 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1526 })?;
1527 if !matches!(
1528 &object.kind,
1529 ObjectKind::Constraint {
1530 constraint: Constraint::Distance(_)
1531 | Constraint::HorizontalDistance(_)
1532 | Constraint::VerticalDistance(_)
1533 | Constraint::Radius(_)
1534 | Constraint::Diameter(_),
1535 }
1536 ) {
1537 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1538 "Object does not support labelPosition: {constraint_id:?}"
1539 ))));
1540 }
1541
1542 let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1543 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1544 "Could not convert label position to AST: {err}"
1545 )))
1546 })?;
1547 let mut new_ast = self.program.ast.clone();
1548 self.mutate_ast(
1549 &mut new_ast,
1550 constraint_id,
1551 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1552 )
1553 .map_err(KclErrorWithOutputs::no_outputs)?;
1554 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1555
1556 self.execute_after_edit(
1557 ctx,
1558 sketch,
1559 sketch_block_ref,
1560 &mut new_ast,
1561 ExecuteAfterEditOptions {
1562 segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1563 edit_kind: EditDeleteKind::Edit,
1564 commit_solved_initial_guesses,
1565 },
1566 )
1567 .await
1568 }
1569
1570 async fn batch_split_segment_operations(
1578 &mut self,
1579 ctx: &ExecutorContext,
1580 _version: Version,
1581 sketch: ObjectId,
1582 edit_segments: Vec<ExistingSegmentCtor>,
1583 add_constraints: Vec<Constraint>,
1584 delete_constraint_ids: Vec<ObjectId>,
1585 _new_segment_info: sketch::NewSegmentInfo,
1586 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1587 let sketch_block_ref =
1589 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1590
1591 let mut new_ast = self.program.ast.clone();
1592 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1593
1594 for segment in edit_segments {
1596 segment_ids_edited.insert(segment.id);
1597 match segment.ctor {
1598 SegmentCtor::Point(ctor) => self
1599 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1600 .map_err(KclErrorWithOutputs::no_outputs)?,
1601 SegmentCtor::Line(ctor) => self
1602 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1603 .map_err(KclErrorWithOutputs::no_outputs)?,
1604 SegmentCtor::Arc(ctor) => self
1605 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1606 .map_err(KclErrorWithOutputs::no_outputs)?,
1607 SegmentCtor::Circle(ctor) => self
1608 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1609 .map_err(KclErrorWithOutputs::no_outputs)?,
1610 SegmentCtor::ControlPointSpline(ctor) => self
1611 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1612 .map_err(KclErrorWithOutputs::no_outputs)?,
1613 }
1614 }
1615
1616 for constraint in add_constraints {
1618 match constraint {
1619 Constraint::Coincident(coincident) => {
1620 self.add_coincident(sketch, coincident, &mut new_ast)
1621 .await
1622 .map_err(KclErrorWithOutputs::no_outputs)?;
1623 }
1624 Constraint::Distance(distance) => {
1625 self.add_distance(sketch, distance, &mut new_ast)
1626 .await
1627 .map_err(KclErrorWithOutputs::no_outputs)?;
1628 }
1629 Constraint::EqualRadius(equal_radius) => {
1630 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1631 .await
1632 .map_err(KclErrorWithOutputs::no_outputs)?;
1633 }
1634 Constraint::Fixed(fixed) => {
1635 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1636 .await
1637 .map_err(KclErrorWithOutputs::no_outputs)?;
1638 }
1639 Constraint::HorizontalDistance(distance) => {
1640 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1641 .await
1642 .map_err(KclErrorWithOutputs::no_outputs)?;
1643 }
1644 Constraint::VerticalDistance(distance) => {
1645 self.add_vertical_distance(sketch, distance, &mut new_ast)
1646 .await
1647 .map_err(KclErrorWithOutputs::no_outputs)?;
1648 }
1649 Constraint::Horizontal(horizontal) => {
1650 self.add_horizontal(sketch, horizontal, &mut new_ast)
1651 .await
1652 .map_err(KclErrorWithOutputs::no_outputs)?;
1653 }
1654 Constraint::LinesEqualLength(lines_equal_length) => {
1655 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1656 .await
1657 .map_err(KclErrorWithOutputs::no_outputs)?;
1658 }
1659 Constraint::Midpoint(midpoint) => {
1660 self.add_midpoint(sketch, midpoint, &mut new_ast)
1661 .await
1662 .map_err(KclErrorWithOutputs::no_outputs)?;
1663 }
1664 Constraint::Parallel(parallel) => {
1665 self.add_parallel(sketch, parallel, &mut new_ast)
1666 .await
1667 .map_err(KclErrorWithOutputs::no_outputs)?;
1668 }
1669 Constraint::Perpendicular(perpendicular) => {
1670 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1671 .await
1672 .map_err(KclErrorWithOutputs::no_outputs)?;
1673 }
1674 Constraint::Vertical(vertical) => {
1675 self.add_vertical(sketch, vertical, &mut new_ast)
1676 .await
1677 .map_err(KclErrorWithOutputs::no_outputs)?;
1678 }
1679 Constraint::Diameter(diameter) => {
1680 self.add_diameter(sketch, diameter, &mut new_ast)
1681 .await
1682 .map_err(KclErrorWithOutputs::no_outputs)?;
1683 }
1684 Constraint::Radius(radius) => {
1685 self.add_radius(sketch, radius, &mut new_ast)
1686 .await
1687 .map_err(KclErrorWithOutputs::no_outputs)?;
1688 }
1689 Constraint::Symmetric(symmetric) => {
1690 self.add_symmetric(sketch, symmetric, &mut new_ast)
1691 .await
1692 .map_err(KclErrorWithOutputs::no_outputs)?;
1693 }
1694 Constraint::Angle(angle) => {
1695 self.add_angle(sketch, angle, &mut new_ast)
1696 .await
1697 .map_err(KclErrorWithOutputs::no_outputs)?;
1698 }
1699 Constraint::Tangent(tangent) => {
1700 self.add_tangent(sketch, tangent, &mut new_ast)
1701 .await
1702 .map_err(KclErrorWithOutputs::no_outputs)?;
1703 }
1704 }
1705 }
1706
1707 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1709
1710 let has_constraint_deletions = !constraint_ids_set.is_empty();
1711 for constraint_id in constraint_ids_set {
1712 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1713 .map_err(KclErrorWithOutputs::no_outputs)?;
1714 }
1715
1716 let (source_delta, mut scene_graph_delta) = self
1720 .execute_after_edit(
1721 ctx,
1722 sketch,
1723 sketch_block_ref,
1724 &mut new_ast,
1725 ExecuteAfterEditOptions {
1726 segment_ids_edited,
1727 edit_kind: EditDeleteKind::Edit,
1728 commit_solved_initial_guesses: true,
1729 },
1730 )
1731 .await?;
1732
1733 if has_constraint_deletions {
1736 scene_graph_delta.invalidates_ids = true;
1737 }
1738
1739 Ok((source_delta, scene_graph_delta))
1740 }
1741
1742 async fn batch_tail_cut_operations(
1743 &mut self,
1744 ctx: &ExecutorContext,
1745 _version: Version,
1746 sketch: ObjectId,
1747 edit_segments: Vec<ExistingSegmentCtor>,
1748 add_constraints: Vec<Constraint>,
1749 delete_constraint_ids: Vec<ObjectId>,
1750 additional_edited_segment_ids: Vec<ObjectId>,
1751 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1752 let sketch_block_ref =
1753 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1754
1755 let mut new_ast = self.program.ast.clone();
1756 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1757
1758 for segment in edit_segments {
1760 segment_ids_edited.insert(segment.id);
1761 match segment.ctor {
1762 SegmentCtor::Point(ctor) => self
1763 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1764 .map_err(KclErrorWithOutputs::no_outputs)?,
1765 SegmentCtor::Line(ctor) => self
1766 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1767 .map_err(KclErrorWithOutputs::no_outputs)?,
1768 SegmentCtor::Arc(ctor) => self
1769 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1770 .map_err(KclErrorWithOutputs::no_outputs)?,
1771 SegmentCtor::Circle(ctor) => self
1772 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1773 .map_err(KclErrorWithOutputs::no_outputs)?,
1774 SegmentCtor::ControlPointSpline(ctor) => self
1775 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1776 .map_err(KclErrorWithOutputs::no_outputs)?,
1777 }
1778 }
1779
1780 segment_ids_edited.extend(additional_edited_segment_ids);
1781
1782 for constraint in add_constraints {
1784 match constraint {
1785 Constraint::Coincident(coincident) => {
1786 self.add_coincident(sketch, coincident, &mut new_ast)
1787 .await
1788 .map_err(KclErrorWithOutputs::no_outputs)?;
1789 }
1790 other => {
1791 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1792 "unsupported constraint in tail cut batch: {other:?}"
1793 ))));
1794 }
1795 }
1796 }
1797
1798 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1800
1801 let has_constraint_deletions = !constraint_ids_set.is_empty();
1802 for constraint_id in constraint_ids_set {
1803 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1804 .map_err(KclErrorWithOutputs::no_outputs)?;
1805 }
1806
1807 let (source_delta, mut scene_graph_delta) = self
1811 .execute_after_edit(
1812 ctx,
1813 sketch,
1814 sketch_block_ref,
1815 &mut new_ast,
1816 ExecuteAfterEditOptions {
1817 segment_ids_edited,
1818 edit_kind: EditDeleteKind::Edit,
1819 commit_solved_initial_guesses: true,
1820 },
1821 )
1822 .await?;
1823
1824 if has_constraint_deletions {
1827 scene_graph_delta.invalidates_ids = true;
1828 }
1829
1830 Ok((source_delta, scene_graph_delta))
1831 }
1832}
1833
1834impl FrontendState {
1835 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1836 self.program = program.clone();
1837
1838 self.point_freedom_cache.clear();
1849 match ctx.run_with_caching(program).await {
1850 Ok(outcome) => {
1851 let outcome = self.update_state_after_exec(outcome, true);
1852 let checkpoint_id = self
1853 .create_sketch_checkpoint(outcome.clone())
1854 .await
1855 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1856 Ok(SetProgramOutcome::Success {
1857 scene_graph: Box::new(self.scene_graph_for_ui()),
1858 exec_outcome: Box::new(outcome),
1859 checkpoint_id: Some(checkpoint_id),
1860 })
1861 }
1862 Err(mut err) => {
1863 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1866 self.update_state_after_exec(outcome, true);
1867 err.scene_graph = Some(self.scene_graph_for_ui());
1868 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1869 }
1870 }
1871 }
1872
1873 pub async fn engine_execute(
1876 &mut self,
1877 ctx: &ExecutorContext,
1878 program: Program,
1879 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1880 self.program = program.clone();
1881
1882 self.point_freedom_cache.clear();
1886 match ctx.run_with_caching(program).await {
1887 Ok(outcome) => {
1888 let outcome = self.update_state_after_exec(outcome, true);
1889 Ok(SceneGraphDelta {
1890 new_graph: self.scene_graph_for_ui(),
1891 exec_outcome: outcome,
1892 new_objects: Default::default(),
1894 invalidates_ids: Default::default(),
1896 })
1897 }
1898 Err(mut err) => {
1899 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1901 self.update_state_after_exec(outcome, true);
1902 err.scene_graph = Some(self.scene_graph_for_ui());
1903 Err(err)
1904 }
1905 }
1906 }
1907
1908 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1909 if matches!(err.error, KclError::EngineHangup { .. }) {
1910 return Err(err);
1914 }
1915
1916 let KclErrorWithOutputs {
1917 error,
1918 mut non_fatal,
1919 variables,
1920 operations,
1921 artifact_graph,
1922 scene_objects,
1923 source_range_to_object,
1924 var_solutions,
1925 filenames,
1926 default_planes,
1927 ..
1928 } = err;
1929
1930 if let Some(source_range) = error.source_ranges().first() {
1931 non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1932 } else {
1933 non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1934 }
1935
1936 Ok(ExecOutcome {
1937 variables,
1938 filenames,
1939 operations,
1940 artifact_graph,
1941 scene_objects,
1942 source_range_to_object,
1943 var_solutions,
1944 issues: non_fatal,
1945 default_planes,
1946 })
1947 }
1948
1949 async fn add_point(
1950 &mut self,
1951 ctx: &ExecutorContext,
1952 sketch: ObjectId,
1953 ctor: PointCtor,
1954 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1955 let at_ast = to_ast_point2d(&ctor.position)
1957 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1958 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1959 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1960 unlabeled: None,
1961 arguments: vec![ast::LabeledArg {
1962 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1963 arg: at_ast,
1964 }],
1965 digest: None,
1966 non_code_meta: Default::default(),
1967 })));
1968
1969 let sketch_id = sketch;
1971 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1972 #[cfg(target_arch = "wasm32")]
1973 web_sys::console::error_1(
1974 &format!(
1975 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1976 &self.scene_graph.objects
1977 )
1978 .into(),
1979 );
1980 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1981 })?;
1982 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1983 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1984 "Object is not a sketch, it is {}",
1985 sketch_object.kind.human_friendly_kind_with_article(),
1986 ))));
1987 };
1988 let mut new_ast = self.program.ast.clone();
1990 let (sketch_block_ref, _) = self
1991 .mutate_ast(
1992 &mut new_ast,
1993 sketch_id,
1994 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1995 )
1996 .map_err(KclErrorWithOutputs::no_outputs)?;
1997 let new_source = source_from_ast(&new_ast);
1999 let (new_program, errors) = Program::parse(&new_source)
2001 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2002 if !errors.is_empty() {
2003 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2004 "Error parsing KCL source after adding point: {errors:?}"
2005 ))));
2006 }
2007 let Some(new_program) = new_program else {
2008 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2009 "No AST produced after adding point".to_string(),
2010 )));
2011 };
2012
2013 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2014 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2015 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2016 )))
2017 })?;
2018
2019 self.program = new_program.clone();
2021
2022 let mut truncated_program = new_program;
2024 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2025 .map_err(KclErrorWithOutputs::no_outputs)?;
2026
2027 let outcome = ctx
2029 .run_mock(
2030 &truncated_program,
2031 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2032 )
2033 .await?;
2034
2035 let new_object_ids = {
2036 let make_err =
2037 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2038 let segment_id = outcome
2039 .source_range_to_object
2040 .get(&point_node_ref.range)
2041 .copied()
2042 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2043 let segment_object = outcome
2044 .scene_objects
2045 .get(segment_id.0)
2046 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2047 let ObjectKind::Segment { segment } = &segment_object.kind else {
2048 return Err(make_err(format!(
2049 "Object is not a segment, it is {}",
2050 segment_object.kind.human_friendly_kind_with_article()
2051 )));
2052 };
2053 let Segment::Point(_) = segment else {
2054 return Err(make_err(format!(
2055 "Segment is not a point, it is {}",
2056 segment.human_friendly_kind_with_article()
2057 )));
2058 };
2059 vec![segment_id]
2060 };
2061 let src_delta = SourceDelta { text: new_source };
2062 let outcome = self.update_state_after_exec(outcome, false);
2064 let scene_graph_delta = SceneGraphDelta {
2065 new_graph: self.scene_graph_for_ui(),
2066 invalidates_ids: false,
2067 new_objects: new_object_ids,
2068 exec_outcome: outcome,
2069 };
2070 Ok((src_delta, scene_graph_delta))
2071 }
2072
2073 async fn add_line(
2074 &mut self,
2075 ctx: &ExecutorContext,
2076 sketch: ObjectId,
2077 ctor: LineCtor,
2078 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2079 let start_ast = to_ast_point2d(&ctor.start)
2081 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2082 let end_ast = to_ast_point2d(&ctor.end)
2083 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2084 let mut arguments = vec![
2085 ast::LabeledArg {
2086 label: Some(ast::Identifier::new(LINE_START_PARAM)),
2087 arg: start_ast,
2088 },
2089 ast::LabeledArg {
2090 label: Some(ast::Identifier::new(LINE_END_PARAM)),
2091 arg: end_ast,
2092 },
2093 ];
2094 if ctor.construction == Some(true) {
2096 arguments.push(ast::LabeledArg {
2097 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2098 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2099 value: ast::LiteralValue::Bool(true),
2100 raw: "true".to_string(),
2101 digest: None,
2102 }))),
2103 });
2104 }
2105 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2106 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2107 unlabeled: None,
2108 arguments,
2109 digest: None,
2110 non_code_meta: Default::default(),
2111 })));
2112
2113 let sketch_id = sketch;
2115 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2116 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2117 })?;
2118 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2119 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2120 "Object is not a sketch, it is {}",
2121 sketch_object.kind.human_friendly_kind_with_article(),
2122 ))));
2123 };
2124 let mut new_ast = self.program.ast.clone();
2126 let (sketch_block_ref, _) = self
2127 .mutate_ast(
2128 &mut new_ast,
2129 sketch_id,
2130 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2131 )
2132 .map_err(KclErrorWithOutputs::no_outputs)?;
2133 let new_source = source_from_ast(&new_ast);
2135 let (new_program, errors) = Program::parse(&new_source)
2137 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2138 if !errors.is_empty() {
2139 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2140 "Error parsing KCL source after adding line: {errors:?}"
2141 ))));
2142 }
2143 let Some(new_program) = new_program else {
2144 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2145 "No AST produced after adding line".to_string(),
2146 )));
2147 };
2148
2149 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2150 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2151 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2152 )))
2153 })?;
2154
2155 self.program = new_program.clone();
2157
2158 let mut truncated_program = new_program;
2160 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2161 .map_err(KclErrorWithOutputs::no_outputs)?;
2162
2163 let outcome = ctx
2165 .run_mock(
2166 &truncated_program,
2167 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2168 )
2169 .await?;
2170
2171 let new_object_ids = {
2172 let make_err =
2173 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2174 let segment_id = outcome
2175 .source_range_to_object
2176 .get(&line_node_ref.range)
2177 .copied()
2178 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2179 let segment_object = outcome
2180 .scene_object_by_id(segment_id)
2181 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2182 let ObjectKind::Segment { segment } = &segment_object.kind else {
2183 return Err(make_err(format!(
2184 "Object is not a segment, it is {}",
2185 segment_object.kind.human_friendly_kind_with_article()
2186 )));
2187 };
2188 let Segment::Line(line) = segment else {
2189 return Err(make_err(format!(
2190 "Segment is not a line, it is {}",
2191 segment.human_friendly_kind_with_article()
2192 )));
2193 };
2194 vec![line.start, line.end, segment_id]
2195 };
2196 let src_delta = SourceDelta { text: new_source };
2197 let outcome = self.update_state_after_exec(outcome, false);
2199 let scene_graph_delta = SceneGraphDelta {
2200 new_graph: self.scene_graph_for_ui(),
2201 invalidates_ids: false,
2202 new_objects: new_object_ids,
2203 exec_outcome: outcome,
2204 };
2205 Ok((src_delta, scene_graph_delta))
2206 }
2207
2208 async fn add_arc(
2209 &mut self,
2210 ctx: &ExecutorContext,
2211 sketch: ObjectId,
2212 ctor: ArcCtor,
2213 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2214 let start_ast = to_ast_point2d(&ctor.start)
2216 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2217 let end_ast = to_ast_point2d(&ctor.end)
2218 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2219 let center_ast = to_ast_point2d(&ctor.center)
2220 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2221 let mut arguments = vec![
2222 ast::LabeledArg {
2223 label: Some(ast::Identifier::new(ARC_START_PARAM)),
2224 arg: start_ast,
2225 },
2226 ast::LabeledArg {
2227 label: Some(ast::Identifier::new(ARC_END_PARAM)),
2228 arg: end_ast,
2229 },
2230 ast::LabeledArg {
2231 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2232 arg: center_ast,
2233 },
2234 ];
2235 if ctor.construction == Some(true) {
2237 arguments.push(ast::LabeledArg {
2238 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2239 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2240 value: ast::LiteralValue::Bool(true),
2241 raw: "true".to_string(),
2242 digest: None,
2243 }))),
2244 });
2245 }
2246 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2247 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2248 unlabeled: None,
2249 arguments,
2250 digest: None,
2251 non_code_meta: Default::default(),
2252 })));
2253
2254 let sketch_id = sketch;
2256 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2257 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2258 })?;
2259 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2260 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2261 "Object is not a sketch, it is {}",
2262 sketch_object.kind.human_friendly_kind_with_article(),
2263 ))));
2264 };
2265 let mut new_ast = self.program.ast.clone();
2267 let (sketch_block_ref, _) = self
2268 .mutate_ast(
2269 &mut new_ast,
2270 sketch_id,
2271 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2272 )
2273 .map_err(KclErrorWithOutputs::no_outputs)?;
2274 let new_source = source_from_ast(&new_ast);
2276 let (new_program, errors) = Program::parse(&new_source)
2278 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2279 if !errors.is_empty() {
2280 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2281 "Error parsing KCL source after adding arc: {errors:?}"
2282 ))));
2283 }
2284 let Some(new_program) = new_program else {
2285 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2286 "No AST produced after adding arc".to_string(),
2287 )));
2288 };
2289
2290 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2291 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2292 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2293 )))
2294 })?;
2295
2296 self.program = new_program.clone();
2298
2299 let mut truncated_program = new_program;
2301 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2302 .map_err(KclErrorWithOutputs::no_outputs)?;
2303
2304 let outcome = ctx
2306 .run_mock(
2307 &truncated_program,
2308 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2309 )
2310 .await?;
2311
2312 let new_object_ids = {
2313 let make_err =
2314 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2315 let segment_id = outcome
2316 .source_range_to_object
2317 .get(&arc_node_ref.range)
2318 .copied()
2319 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2320 let segment_object = outcome
2321 .scene_objects
2322 .get(segment_id.0)
2323 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2324 let ObjectKind::Segment { segment } = &segment_object.kind else {
2325 return Err(make_err(format!(
2326 "Object is not a segment, it is {}",
2327 segment_object.kind.human_friendly_kind_with_article()
2328 )));
2329 };
2330 let Segment::Arc(arc) = segment else {
2331 return Err(make_err(format!(
2332 "Segment is not an arc, it is {}",
2333 segment.human_friendly_kind_with_article()
2334 )));
2335 };
2336 vec![arc.start, arc.end, arc.center, segment_id]
2337 };
2338 let src_delta = SourceDelta { text: new_source };
2339 let outcome = self.update_state_after_exec(outcome, false);
2341 let scene_graph_delta = SceneGraphDelta {
2342 new_graph: self.scene_graph_for_ui(),
2343 invalidates_ids: false,
2344 new_objects: new_object_ids,
2345 exec_outcome: outcome,
2346 };
2347 Ok((src_delta, scene_graph_delta))
2348 }
2349
2350 async fn add_circle(
2351 &mut self,
2352 ctx: &ExecutorContext,
2353 sketch: ObjectId,
2354 ctor: CircleCtor,
2355 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2356 let start_ast = to_ast_point2d(&ctor.start)
2358 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2359 let center_ast = to_ast_point2d(&ctor.center)
2360 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2361 let mut arguments = vec![
2362 ast::LabeledArg {
2363 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2364 arg: start_ast,
2365 },
2366 ast::LabeledArg {
2367 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2368 arg: center_ast,
2369 },
2370 ];
2371 if ctor.construction == Some(true) {
2373 arguments.push(ast::LabeledArg {
2374 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2375 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2376 value: ast::LiteralValue::Bool(true),
2377 raw: "true".to_string(),
2378 digest: None,
2379 }))),
2380 });
2381 }
2382 let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2383 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2384 unlabeled: None,
2385 arguments,
2386 digest: None,
2387 non_code_meta: Default::default(),
2388 })));
2389
2390 let sketch_id = sketch;
2392 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2393 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2394 })?;
2395 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2396 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2397 "Object is not a sketch, it is {}",
2398 sketch_object.kind.human_friendly_kind_with_article(),
2399 ))));
2400 };
2401 let mut new_ast = self.program.ast.clone();
2403 let (sketch_block_ref, _) = self
2404 .mutate_ast(
2405 &mut new_ast,
2406 sketch_id,
2407 AstMutateCommand::AddSketchBlockVarDecl {
2408 prefix: CIRCLE_VARIABLE.to_owned(),
2409 expr: circle_ast,
2410 },
2411 )
2412 .map_err(KclErrorWithOutputs::no_outputs)?;
2413 let new_source = source_from_ast(&new_ast);
2415 let (new_program, errors) = Program::parse(&new_source)
2417 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2418 if !errors.is_empty() {
2419 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2420 "Error parsing KCL source after adding circle: {errors:?}"
2421 ))));
2422 }
2423 let Some(new_program) = new_program else {
2424 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2425 "No AST produced after adding circle".to_string(),
2426 )));
2427 };
2428
2429 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2430 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2431 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2432 )))
2433 })?;
2434
2435 self.program = new_program.clone();
2437
2438 let mut truncated_program = new_program;
2440 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2441 .map_err(KclErrorWithOutputs::no_outputs)?;
2442
2443 let outcome = ctx
2445 .run_mock(
2446 &truncated_program,
2447 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2448 )
2449 .await?;
2450
2451 let new_object_ids = {
2452 let make_err =
2453 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2454 let segment_id = outcome
2455 .source_range_to_object
2456 .get(&circle_node_ref.range)
2457 .copied()
2458 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2459 let segment_object = outcome
2460 .scene_objects
2461 .get(segment_id.0)
2462 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2463 let ObjectKind::Segment { segment } = &segment_object.kind else {
2464 return Err(make_err(format!(
2465 "Object is not a segment, it is {}",
2466 segment_object.kind.human_friendly_kind_with_article()
2467 )));
2468 };
2469 let Segment::Circle(circle) = segment else {
2470 return Err(make_err(format!(
2471 "Segment is not a circle, it is {}",
2472 segment.human_friendly_kind_with_article()
2473 )));
2474 };
2475 vec![circle.start, circle.center, segment_id]
2476 };
2477 let src_delta = SourceDelta { text: new_source };
2478 let outcome = self.update_state_after_exec(outcome, false);
2480 let scene_graph_delta = SceneGraphDelta {
2481 new_graph: self.scene_graph_for_ui(),
2482 invalidates_ids: false,
2483 new_objects: new_object_ids,
2484 exec_outcome: outcome,
2485 };
2486 Ok((src_delta, scene_graph_delta))
2487 }
2488
2489 async fn add_control_point_spline(
2490 &mut self,
2491 ctx: &ExecutorContext,
2492 sketch: ObjectId,
2493 ctor: ControlPointSplineCtor,
2494 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2495 let new_program = ensure_control_point_spline_experimental_features(&self.program)
2496 .map_err(KclErrorWithOutputs::no_outputs)?;
2497
2498 let points_ast = to_ast_point2d_array(&ctor.points)
2499 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2500 let mut arguments = vec![ast::LabeledArg {
2501 label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2502 arg: points_ast,
2503 }];
2504 if ctor.construction == Some(true) {
2505 arguments.push(ast::LabeledArg {
2506 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2507 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2508 value: ast::LiteralValue::Bool(true),
2509 raw: "true".to_string(),
2510 digest: None,
2511 }))),
2512 });
2513 }
2514 let spline_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2515 callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2516 unlabeled: None,
2517 arguments,
2518 digest: None,
2519 non_code_meta: Default::default(),
2520 })));
2521
2522 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2523 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2524 })?;
2525 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2526 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2527 "Object is not a sketch, it is {}",
2528 sketch_object.kind.human_friendly_kind_with_article(),
2529 ))));
2530 };
2531
2532 let mut new_ast = new_program.ast.clone();
2533 let (sketch_block_ref, _) = self
2534 .mutate_ast(
2535 &mut new_ast,
2536 sketch,
2537 AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2538 )
2539 .map_err(KclErrorWithOutputs::no_outputs)?;
2540 let new_source = source_from_ast(&new_ast);
2541 let (new_program, errors) = Program::parse(&new_source)
2542 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2543 if !errors.is_empty() {
2544 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2545 "Error parsing KCL source after adding controlPointSpline: {errors:?}"
2546 ))));
2547 }
2548 let Some(new_program) = new_program else {
2549 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2550 "No AST produced after adding controlPointSpline".to_string(),
2551 )));
2552 };
2553
2554 let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2555 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2556 "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2557 )))
2558 })?;
2559
2560 self.program = new_program.clone();
2561
2562 let mut truncated_program = new_program;
2563 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2564 .map_err(KclErrorWithOutputs::no_outputs)?;
2565
2566 let outcome = ctx
2567 .run_mock(
2568 &truncated_program,
2569 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2570 )
2571 .await?;
2572
2573 let new_object_ids = {
2574 let make_err =
2575 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2576 let segment_id = outcome
2577 .source_range_to_object
2578 .get(&spline_node_ref.range)
2579 .copied()
2580 .ok_or_else(|| {
2581 make_err(format!(
2582 "Source range of controlPointSpline not found: {spline_node_ref:?}"
2583 ))
2584 })?;
2585 let segment_object = outcome
2586 .scene_objects
2587 .get(segment_id.0)
2588 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2589 let ObjectKind::Segment { segment } = &segment_object.kind else {
2590 return Err(make_err(format!(
2591 "Object is not a segment, it is {}",
2592 segment_object.kind.human_friendly_kind_with_article()
2593 )));
2594 };
2595 let Segment::ControlPointSpline(spline) = segment else {
2596 return Err(make_err(format!(
2597 "Segment is not a control point spline, it is {}",
2598 segment.human_friendly_kind_with_article()
2599 )));
2600 };
2601
2602 let mut ids = outcome
2603 .scene_objects
2604 .iter()
2605 .filter_map(|obj| match &obj.kind {
2606 ObjectKind::Segment {
2607 segment: Segment::Line(line),
2608 } if line.owner == Some(segment_id) => Some(obj.id),
2609 _ => None,
2610 })
2611 .collect::<Vec<_>>();
2612 ids.extend(spline.controls.clone());
2613 ids.push(segment_id);
2614 ids
2615 };
2616 let src_delta = SourceDelta { text: new_source };
2617 let outcome = self.update_state_after_exec(outcome, false);
2618 let scene_graph_delta = SceneGraphDelta {
2619 new_graph: self.scene_graph_for_ui(),
2620 invalidates_ids: false,
2621 new_objects: new_object_ids,
2622 exec_outcome: outcome,
2623 };
2624 Ok((src_delta, scene_graph_delta))
2625 }
2626
2627 fn edit_point(
2628 &mut self,
2629 new_ast: &mut ast::Node<ast::Program>,
2630 sketch: ObjectId,
2631 point: ObjectId,
2632 ctor: PointCtor,
2633 ) -> Result<(), KclError> {
2634 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2636
2637 let sketch_id = sketch;
2639 let sketch_object = self
2640 .scene_graph
2641 .objects
2642 .get(sketch_id.0)
2643 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2644 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2645 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2646 };
2647 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2648 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2649 })?;
2650 let point_id = point;
2652 let point_object = self
2653 .scene_graph
2654 .objects
2655 .get(point_id.0)
2656 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2657 let ObjectKind::Segment {
2658 segment: Segment::Point(point),
2659 } = &point_object.kind
2660 else {
2661 return Err(KclError::refactor(format!(
2662 "Object is not a point segment: {point_object:?}"
2663 )));
2664 };
2665
2666 if let Some(owner_id) = point.owner {
2668 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2669 KclError::refactor(format!(
2670 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2671 ))
2672 })?;
2673 let ObjectKind::Segment { segment } = &owner_object.kind else {
2674 return Err(KclError::refactor(format!(
2675 "Internal: Owner of point is not a segment, but found {}",
2676 owner_object.kind.human_friendly_kind_with_article()
2677 )));
2678 };
2679
2680 if let Segment::Line(line) = segment {
2682 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2683 return Err(KclError::refactor(format!(
2684 "Internal: Owner of point does not have line ctor, but found {}",
2685 line.ctor.human_friendly_kind_with_article()
2686 )));
2687 };
2688 let mut line_ctor = line_ctor.clone();
2689 if line.start == point_id {
2691 line_ctor.start = ctor.position;
2692 } else if line.end == point_id {
2693 line_ctor.end = ctor.position;
2694 } else {
2695 return Err(KclError::refactor(format!(
2696 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2697 )));
2698 }
2699 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2700 }
2701
2702 if let Segment::Arc(arc) = segment {
2704 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2705 return Err(KclError::refactor(format!(
2706 "Internal: Owner of point does not have arc ctor, but found {}",
2707 arc.ctor.human_friendly_kind_with_article()
2708 )));
2709 };
2710 let mut arc_ctor = arc_ctor.clone();
2711 if arc.center == point_id {
2713 arc_ctor.center = ctor.position;
2714 } else if arc.start == point_id {
2715 arc_ctor.start = ctor.position;
2716 } else if arc.end == point_id {
2717 arc_ctor.end = ctor.position;
2718 } else {
2719 return Err(KclError::refactor(format!(
2720 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2721 )));
2722 }
2723 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2724 }
2725
2726 if let Segment::Circle(circle) = segment {
2728 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2729 return Err(KclError::refactor(format!(
2730 "Internal: Owner of point does not have circle ctor, but found {}",
2731 circle.ctor.human_friendly_kind_with_article()
2732 )));
2733 };
2734 let mut circle_ctor = circle_ctor.clone();
2735 if circle.center == point_id {
2736 circle_ctor.center = ctor.position;
2737 } else if circle.start == point_id {
2738 circle_ctor.start = ctor.position;
2739 } else {
2740 return Err(KclError::refactor(format!(
2741 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2742 )));
2743 }
2744 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2745 }
2746
2747 if let Segment::ControlPointSpline(spline) = segment {
2748 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2749 return Err(KclError::refactor(format!(
2750 "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2751 spline.ctor.human_friendly_kind_with_article()
2752 )));
2753 };
2754 let mut spline_ctor = spline_ctor.clone();
2755 let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2756 return Err(KclError::refactor(format!(
2757 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2758 )));
2759 };
2760 spline_ctor.points[control_index] = ctor.position;
2761 return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2762 }
2763
2764 }
2767
2768 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2770 Ok(())
2771 }
2772
2773 fn edit_line(
2774 &mut self,
2775 new_ast: &mut ast::Node<ast::Program>,
2776 sketch: ObjectId,
2777 line: ObjectId,
2778 ctor: LineCtor,
2779 ) -> Result<(), KclError> {
2780 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2782 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2783
2784 let sketch_id = sketch;
2786 let sketch_object = self
2787 .scene_graph
2788 .objects
2789 .get(sketch_id.0)
2790 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2791 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2792 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2793 };
2794 sketch
2795 .segments
2796 .iter()
2797 .find(|o| **o == line)
2798 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2799 let line_id = line;
2801 let line_object = self
2802 .scene_graph
2803 .objects
2804 .get(line_id.0)
2805 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2806 let ObjectKind::Segment { .. } = &line_object.kind else {
2807 let kind = line_object.kind.human_friendly_kind_with_article();
2808 return Err(KclError::refactor(format!(
2809 "This constraint only works on Segments, but you selected {kind}"
2810 )));
2811 };
2812
2813 self.mutate_ast(
2815 new_ast,
2816 line_id,
2817 AstMutateCommand::EditLine {
2818 start: new_start_ast,
2819 end: new_end_ast,
2820 construction: ctor.construction,
2821 },
2822 )?;
2823 Ok(())
2824 }
2825
2826 fn edit_arc(
2827 &mut self,
2828 new_ast: &mut ast::Node<ast::Program>,
2829 sketch: ObjectId,
2830 arc: ObjectId,
2831 ctor: ArcCtor,
2832 ) -> Result<(), KclError> {
2833 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2835 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2836 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2837
2838 let sketch_id = sketch;
2840 let sketch_object = self
2841 .scene_graph
2842 .objects
2843 .get(sketch_id.0)
2844 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2845 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2846 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2847 };
2848 sketch
2849 .segments
2850 .iter()
2851 .find(|o| **o == arc)
2852 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2853 let arc_id = arc;
2855 let arc_object = self
2856 .scene_graph
2857 .objects
2858 .get(arc_id.0)
2859 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2860 let ObjectKind::Segment { .. } = &arc_object.kind else {
2861 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2862 };
2863
2864 self.mutate_ast(
2866 new_ast,
2867 arc_id,
2868 AstMutateCommand::EditArc {
2869 start: new_start_ast,
2870 end: new_end_ast,
2871 center: new_center_ast,
2872 construction: ctor.construction,
2873 },
2874 )?;
2875 Ok(())
2876 }
2877
2878 fn edit_circle(
2879 &mut self,
2880 new_ast: &mut ast::Node<ast::Program>,
2881 sketch: ObjectId,
2882 circle: ObjectId,
2883 ctor: CircleCtor,
2884 ) -> Result<(), KclError> {
2885 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2887 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2888
2889 let sketch_id = sketch;
2891 let sketch_object = self
2892 .scene_graph
2893 .objects
2894 .get(sketch_id.0)
2895 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2896 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2897 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2898 };
2899 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2900 KclError::refactor(format!(
2901 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2902 ))
2903 })?;
2904 let circle_id = circle;
2906 let circle_object = self
2907 .scene_graph
2908 .objects
2909 .get(circle_id.0)
2910 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2911 let ObjectKind::Segment { .. } = &circle_object.kind else {
2912 return Err(KclError::refactor(format!(
2913 "Object is not a segment: {circle_object:?}"
2914 )));
2915 };
2916
2917 self.mutate_ast(
2919 new_ast,
2920 circle_id,
2921 AstMutateCommand::EditCircle {
2922 start: new_start_ast,
2923 center: new_center_ast,
2924 construction: ctor.construction,
2925 },
2926 )?;
2927 Ok(())
2928 }
2929
2930 fn edit_control_point_spline(
2931 &mut self,
2932 new_ast: &mut ast::Node<ast::Program>,
2933 sketch: ObjectId,
2934 spline: ObjectId,
2935 ctor: ControlPointSplineCtor,
2936 ) -> Result<(), KclError> {
2937 let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
2938
2939 let sketch_object = self
2940 .scene_graph
2941 .objects
2942 .get(sketch.0)
2943 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2944 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2945 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2946 };
2947 sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
2948 KclError::refactor(format!(
2949 "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
2950 ))
2951 })?;
2952
2953 let spline_object =
2954 self.scene_graph.objects.get(spline.0).ok_or_else(|| {
2955 KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
2956 })?;
2957 let ObjectKind::Segment { .. } = &spline_object.kind else {
2958 return Err(KclError::refactor(format!(
2959 "Object is not a segment: {spline_object:?}"
2960 )));
2961 };
2962
2963 self.mutate_ast(
2964 new_ast,
2965 spline,
2966 AstMutateCommand::EditControlPointSpline {
2967 points: points_ast,
2968 construction: ctor.construction,
2969 },
2970 )?;
2971 Ok(())
2972 }
2973
2974 fn delete_segment(
2975 &mut self,
2976 new_ast: &mut ast::Node<ast::Program>,
2977 sketch: ObjectId,
2978 segment_id: ObjectId,
2979 ) -> Result<(), KclError> {
2980 let sketch_id = sketch;
2982 let sketch_object = self
2983 .scene_graph
2984 .objects
2985 .get(sketch_id.0)
2986 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2987 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2988 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2989 };
2990 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2991 KclError::refactor(format!(
2992 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2993 ))
2994 })?;
2995 let segment_object =
2997 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
2998 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
2999 })?;
3000 let ObjectKind::Segment { .. } = &segment_object.kind else {
3001 return Err(KclError::refactor(format!(
3002 "Object is not a segment, it is {}",
3003 segment_object.kind.human_friendly_kind_with_article()
3004 )));
3005 };
3006
3007 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3009 Ok(())
3010 }
3011
3012 fn delete_constraint(
3013 &mut self,
3014 new_ast: &mut ast::Node<ast::Program>,
3015 sketch: ObjectId,
3016 constraint_id: ObjectId,
3017 ) -> Result<(), KclError> {
3018 let sketch_id = sketch;
3020 let sketch_object = self
3021 .scene_graph
3022 .objects
3023 .get(sketch_id.0)
3024 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3025 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3026 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3027 };
3028 sketch
3029 .constraints
3030 .iter()
3031 .find(|o| **o == constraint_id)
3032 .ok_or_else(|| {
3033 KclError::refactor(format!(
3034 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3035 ))
3036 })?;
3037 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3039 KclError::refactor(format!(
3040 "Constraint not found in scene graph: constraint={constraint_id:?}"
3041 ))
3042 })?;
3043 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3044 return Err(KclError::refactor(format!(
3045 "Object is not a constraint, it is {}",
3046 constraint_object.kind.human_friendly_kind_with_article()
3047 )));
3048 };
3049
3050 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3052 Ok(())
3053 }
3054
3055 fn edit_coincident_constraint(
3056 &mut self,
3057 new_ast: &mut ast::Node<ast::Program>,
3058 constraint_id: ObjectId,
3059 segments: Vec<ConstraintSegment>,
3060 ) -> Result<(), KclError> {
3061 if segments.len() < 2 {
3062 return Err(KclError::refactor(format!(
3063 "Coincident constraint must have at least 2 inputs, got {}",
3064 segments.len()
3065 )));
3066 }
3067
3068 let segment_asts = segments
3069 .iter()
3070 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3071 .collect::<Result<Vec<_>, _>>()?;
3072
3073 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3074 elements: segment_asts,
3075 digest: None,
3076 non_code_meta: Default::default(),
3077 })));
3078
3079 self.mutate_ast(
3080 new_ast,
3081 constraint_id,
3082 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3083 )?;
3084 Ok(())
3085 }
3086
3087 fn edit_horizontal_points_constraint(
3088 &mut self,
3089 new_ast: &mut ast::Node<ast::Program>,
3090 constraint_id: ObjectId,
3091 points: Vec<ConstraintSegment>,
3092 ) -> Result<(), KclError> {
3093 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3094 }
3095
3096 fn edit_vertical_points_constraint(
3097 &mut self,
3098 new_ast: &mut ast::Node<ast::Program>,
3099 constraint_id: ObjectId,
3100 points: Vec<ConstraintSegment>,
3101 ) -> Result<(), KclError> {
3102 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3103 }
3104
3105 fn edit_axis_points_constraint(
3106 &mut self,
3107 new_ast: &mut ast::Node<ast::Program>,
3108 constraint_id: ObjectId,
3109 points: Vec<ConstraintSegment>,
3110 constraint_name: &str,
3111 ) -> Result<(), KclError> {
3112 if points.len() < 2 {
3113 return Err(KclError::refactor(format!(
3114 "{constraint_name} points constraint must have at least 2 points, got {}",
3115 points.len()
3116 )));
3117 }
3118
3119 let point_asts = points
3120 .iter()
3121 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3122 .collect::<Result<Vec<_>, _>>()?;
3123
3124 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3125 elements: point_asts,
3126 digest: None,
3127 non_code_meta: Default::default(),
3128 })));
3129
3130 self.mutate_ast(
3131 new_ast,
3132 constraint_id,
3133 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3134 )?;
3135 Ok(())
3136 }
3137
3138 fn edit_equal_length_constraint(
3140 &mut self,
3141 new_ast: &mut ast::Node<ast::Program>,
3142 constraint_id: ObjectId,
3143 lines: Vec<ObjectId>,
3144 ) -> Result<(), KclError> {
3145 if lines.len() < 2 {
3146 return Err(KclError::refactor(format!(
3147 "Lines equal length constraint must have at least 2 lines, got {}",
3148 lines.len()
3149 )));
3150 }
3151
3152 let line_asts = lines
3153 .iter()
3154 .map(|line_id| {
3155 let line_object = self
3156 .scene_graph
3157 .objects
3158 .get(line_id.0)
3159 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3160 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3161 let kind = line_object.kind.human_friendly_kind_with_article();
3162 return Err(KclError::refactor(format!(
3163 "This constraint only works on Segments, but you selected {kind}"
3164 )));
3165 };
3166 let Segment::Line(_) = line_segment else {
3167 let kind = line_segment.human_friendly_kind_with_article();
3168 return Err(KclError::refactor(format!(
3169 "Only lines can be made equal length, but you selected {kind}"
3170 )));
3171 };
3172
3173 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3174 })
3175 .collect::<Result<Vec<_>, _>>()?;
3176
3177 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3178 elements: line_asts,
3179 digest: None,
3180 non_code_meta: Default::default(),
3181 })));
3182
3183 self.mutate_ast(
3184 new_ast,
3185 constraint_id,
3186 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3187 )?;
3188 Ok(())
3189 }
3190
3191 fn edit_parallel_constraint(
3193 &mut self,
3194 new_ast: &mut ast::Node<ast::Program>,
3195 constraint_id: ObjectId,
3196 lines: Vec<ObjectId>,
3197 ) -> Result<(), KclError> {
3198 if lines.len() < 2 {
3199 return Err(KclError::refactor(format!(
3200 "Parallel constraint must have at least 2 lines, got {}",
3201 lines.len()
3202 )));
3203 }
3204
3205 let line_asts = lines
3206 .iter()
3207 .map(|line_id| {
3208 let line_object = self
3209 .scene_graph
3210 .objects
3211 .get(line_id.0)
3212 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3213 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3214 let kind = line_object.kind.human_friendly_kind_with_article();
3215 return Err(KclError::refactor(format!(
3216 "This constraint only works on Segments, but you selected {kind}"
3217 )));
3218 };
3219 let Segment::Line(_) = line_segment else {
3220 let kind = line_segment.human_friendly_kind_with_article();
3221 return Err(KclError::refactor(format!(
3222 "Only lines can be made parallel, but you selected {kind}"
3223 )));
3224 };
3225
3226 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3227 })
3228 .collect::<Result<Vec<_>, _>>()?;
3229
3230 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3231 elements: line_asts,
3232 digest: None,
3233 non_code_meta: Default::default(),
3234 })));
3235
3236 self.mutate_ast(
3237 new_ast,
3238 constraint_id,
3239 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3240 )?;
3241 Ok(())
3242 }
3243
3244 fn edit_equal_radius_constraint(
3246 &mut self,
3247 new_ast: &mut ast::Node<ast::Program>,
3248 constraint_id: ObjectId,
3249 input: Vec<ObjectId>,
3250 ) -> Result<(), KclError> {
3251 if input.len() < 2 {
3252 return Err(KclError::refactor(format!(
3253 "equalRadius constraint must have at least 2 segments, got {}",
3254 input.len()
3255 )));
3256 }
3257
3258 let input_asts = input
3259 .iter()
3260 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3261 .collect::<Result<Vec<_>, _>>()?;
3262
3263 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3264 elements: input_asts,
3265 digest: None,
3266 non_code_meta: Default::default(),
3267 })));
3268
3269 self.mutate_ast(
3270 new_ast,
3271 constraint_id,
3272 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3273 )?;
3274 Ok(())
3275 }
3276
3277 async fn execute_after_edit(
3278 &mut self,
3279 ctx: &ExecutorContext,
3280 sketch: ObjectId,
3281 sketch_block_ref: AstNodeRef,
3282 new_ast: &mut ast::Node<ast::Program>,
3283 options: ExecuteAfterEditOptions,
3284 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3285 let ExecuteAfterEditOptions {
3286 segment_ids_edited,
3287 edit_kind,
3288 commit_solved_initial_guesses,
3289 } = options;
3290
3291 let new_source = source_from_ast(new_ast);
3293 let (new_program, errors) = Program::parse(&new_source)
3295 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3296 if !errors.is_empty() {
3297 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3298 "Error parsing KCL source after editing: {errors:?}"
3299 ))));
3300 }
3301 let Some(new_program) = new_program else {
3302 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3303 "No AST produced after editing".to_string(),
3304 )));
3305 };
3306
3307 self.program = new_program.clone();
3309
3310 let is_delete = edit_kind.is_delete();
3312 let truncated_program = {
3313 let mut truncated_program = new_program;
3314 only_sketch_block(
3315 &mut truncated_program.ast,
3316 &sketch_block_ref,
3317 edit_kind.to_change_kind(),
3318 )
3319 .map_err(KclErrorWithOutputs::no_outputs)?;
3320 truncated_program
3321 };
3322
3323 let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3325 let mock_config = MockConfig {
3326 sketch_block_id: Some(sketch),
3327 freedom_analysis: is_delete,
3328 segment_ids_edited: segment_ids_edited.clone(),
3329 drag_anchors,
3330 ..Default::default()
3331 };
3332 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3333
3334 let outcome = self.update_state_after_exec(outcome, is_delete);
3336
3337 let src_delta = if commit_solved_initial_guesses {
3338 self.commit_var_solutions_to_program(&outcome, "editing")?
3339 } else {
3340 SourceDelta { text: new_source }
3341 };
3342 let scene_graph_delta = SceneGraphDelta {
3343 new_graph: self.scene_graph_for_ui(),
3344 invalidates_ids: is_delete,
3345 new_objects: Vec::new(),
3346 exec_outcome: outcome,
3347 };
3348 Ok((src_delta, scene_graph_delta))
3349 }
3350
3351 async fn execute_after_delete_sketch(
3352 &mut self,
3353 ctx: &ExecutorContext,
3354 new_ast: &mut ast::Node<ast::Program>,
3355 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3356 let new_source = source_from_ast(new_ast);
3358 let (new_program, errors) = Program::parse(&new_source)
3360 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3361 if !errors.is_empty() {
3362 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3363 "Error parsing KCL source after editing: {errors:?}"
3364 ))));
3365 }
3366 let Some(new_program) = new_program else {
3367 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3368 "No AST produced after editing".to_string(),
3369 )));
3370 };
3371
3372 self.program = new_program.clone();
3374
3375 let outcome = ctx.run_with_caching(new_program).await?;
3381 let freedom_analysis_ran = true;
3382
3383 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3384
3385 let src_delta = SourceDelta { text: new_source };
3386 let scene_graph_delta = SceneGraphDelta {
3387 new_graph: self.scene_graph_for_ui(),
3388 invalidates_ids: true,
3389 new_objects: Vec::new(),
3390 exec_outcome: outcome,
3391 };
3392 Ok((src_delta, scene_graph_delta))
3393 }
3394
3395 fn point_id_to_ast_reference(
3400 &self,
3401 point_id: ObjectId,
3402 new_ast: &mut ast::Node<ast::Program>,
3403 ) -> Result<ast::Expr, KclError> {
3404 let point_object = self
3405 .scene_graph
3406 .objects
3407 .get(point_id.0)
3408 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3409 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3410 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3411 };
3412 let Segment::Point(point) = point_segment else {
3413 return Err(KclError::refactor(format!(
3414 "Only points are currently supported: {point_object:?}"
3415 )));
3416 };
3417
3418 if let Some(owner_id) = point.owner {
3419 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3420 KclError::refactor(format!(
3421 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3422 ))
3423 })?;
3424 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3425 return Err(KclError::refactor(format!(
3426 "Owner of point is not a segment, but found {}",
3427 owner_object.kind.human_friendly_kind_with_article()
3428 )));
3429 };
3430
3431 match owner_segment {
3432 Segment::Line(line) => {
3433 let property = if line.start == point_id {
3434 LINE_PROPERTY_START
3435 } else if line.end == point_id {
3436 LINE_PROPERTY_END
3437 } else {
3438 return Err(KclError::refactor(format!(
3439 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3440 )));
3441 };
3442 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3443 }
3444 Segment::Arc(arc) => {
3445 let property = if arc.start == point_id {
3446 ARC_PROPERTY_START
3447 } else if arc.end == point_id {
3448 ARC_PROPERTY_END
3449 } else if arc.center == point_id {
3450 ARC_PROPERTY_CENTER
3451 } else {
3452 return Err(KclError::refactor(format!(
3453 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3454 )));
3455 };
3456 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3457 }
3458 Segment::Circle(circle) => {
3459 let property = if circle.start == point_id {
3460 CIRCLE_PROPERTY_START
3461 } else if circle.center == point_id {
3462 CIRCLE_PROPERTY_CENTER
3463 } else {
3464 return Err(KclError::refactor(format!(
3465 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3466 )));
3467 };
3468 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3469 }
3470 Segment::ControlPointSpline(spline) => {
3471 let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3472 return Err(KclError::refactor(format!(
3473 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3474 )));
3475 };
3476 let owner_expr =
3477 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3478 let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3479 Ok(create_index_expression(controls_expr, index))
3480 }
3481 _ => Err(KclError::refactor(format!(
3482 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3483 ))),
3484 }
3485 } else {
3486 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3488 }
3489 }
3490
3491 fn line_id_to_ast_reference(
3492 &self,
3493 line_id: ObjectId,
3494 new_ast: &mut ast::Node<ast::Program>,
3495 ) -> Result<ast::Expr, KclError> {
3496 let line_object = self
3497 .scene_graph
3498 .objects
3499 .get(line_id.0)
3500 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3501 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3502 return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3503 };
3504 let Segment::Line(line) = line_segment else {
3505 return Err(KclError::refactor(format!(
3506 "Only lines are currently supported: {line_object:?}"
3507 )));
3508 };
3509
3510 if let Some(owner_id) = line.owner {
3511 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3512 KclError::refactor(format!(
3513 "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3514 ))
3515 })?;
3516 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3517 return Err(KclError::refactor(format!(
3518 "Owner of line is not a segment, but found {}",
3519 owner_object.kind.human_friendly_kind_with_article()
3520 )));
3521 };
3522
3523 match owner_segment {
3524 Segment::ControlPointSpline(spline) => {
3525 let Some(index) = spline
3526 .controls
3527 .windows(2)
3528 .position(|window| window[0] == line.start && window[1] == line.end)
3529 else {
3530 return Err(KclError::refactor(format!(
3531 "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3532 )));
3533 };
3534 let owner_expr =
3535 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3536 let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3537 Ok(create_index_expression(edges_expr, index))
3538 }
3539 _ => Err(KclError::refactor(format!(
3540 "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3541 ))),
3542 }
3543 } else {
3544 get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3545 }
3546 }
3547
3548 fn coincident_segment_to_ast(
3549 &self,
3550 segment: &ConstraintSegment,
3551 new_ast: &mut ast::Node<ast::Program>,
3552 ) -> Result<ast::Expr, KclError> {
3553 match segment {
3554 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3555 ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3556 }
3557 }
3558
3559 fn segment_id_to_constraint_ast_reference(
3560 &self,
3561 segment_id: ObjectId,
3562 new_ast: &mut ast::Node<ast::Program>,
3563 ) -> Result<ast::Expr, KclError> {
3564 let segment_object = self
3565 .scene_graph
3566 .objects
3567 .get(segment_id.0)
3568 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3569 let ObjectKind::Segment { segment } = &segment_object.kind else {
3570 return Err(KclError::refactor(format!(
3571 "Object is not a segment, it is {}",
3572 segment_object.kind.human_friendly_kind_with_article()
3573 )));
3574 };
3575
3576 match segment {
3577 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3578 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3579 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3580 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3581 Segment::ControlPointSpline(_) => {
3582 get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3583 }
3584 }
3585 }
3586
3587 fn axis_constraint_segment_to_ast(
3588 &self,
3589 segment: &ConstraintSegment,
3590 new_ast: &mut ast::Node<ast::Program>,
3591 ) -> Result<ast::Expr, KclError> {
3592 match segment {
3593 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3594 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3595 }
3596 }
3597
3598 async fn add_coincident(
3599 &mut self,
3600 sketch: ObjectId,
3601 coincident: Coincident,
3602 new_ast: &mut ast::Node<ast::Program>,
3603 ) -> Result<AstNodeRef, KclError> {
3604 let sketch_id = sketch;
3605 for segment in &coincident.segments {
3606 let ConstraintSegment::Segment(segment_id) = segment else {
3607 continue;
3608 };
3609 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3610 continue;
3611 };
3612 if matches!(
3613 segment_object.kind,
3614 ObjectKind::Segment {
3615 segment: Segment::ControlPointSpline(_)
3616 }
3617 ) {
3618 return Err(KclError::refactor(
3619 "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3620 .to_owned(),
3621 ));
3622 }
3623 }
3624 let segment_asts = coincident
3625 .segments
3626 .iter()
3627 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3628 .collect::<Result<Vec<_>, _>>()?;
3629 if segment_asts.len() < 2 {
3630 return Err(KclError::refactor(format!(
3631 "Coincident constraint must have at least 2 inputs, got {}",
3632 segment_asts.len()
3633 )));
3634 }
3635
3636 let coincident_ast = create_coincident_ast(segment_asts);
3638
3639 let (sketch_block_ref, _) = self.mutate_ast(
3641 new_ast,
3642 sketch_id,
3643 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3644 )?;
3645 Ok(sketch_block_ref)
3646 }
3647
3648 async fn add_distance(
3649 &mut self,
3650 sketch: ObjectId,
3651 distance: Distance,
3652 new_ast: &mut ast::Node<ast::Program>,
3653 ) -> Result<AstNodeRef, KclError> {
3654 let sketch_id = sketch;
3655 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3656 [pt0, pt1] => [
3657 self.coincident_segment_to_ast(pt0, new_ast)?,
3658 self.coincident_segment_to_ast(pt1, new_ast)?,
3659 ],
3660 _ => {
3661 return Err(KclError::refactor(format!(
3662 "Distance constraint must have exactly 2 points, got {}",
3663 distance.points.len()
3664 )));
3665 }
3666 };
3667
3668 let arguments = match &distance.label_position {
3669 Some(label_position) => vec![ast::LabeledArg {
3670 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3671 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3672 }],
3673 None => Default::default(),
3674 };
3675
3676 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3678 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3679 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3680 ast::ArrayExpression {
3681 elements: vec![pt0_ast, pt1_ast],
3682 digest: None,
3683 non_code_meta: Default::default(),
3684 },
3685 )))),
3686 arguments,
3687 digest: None,
3688 non_code_meta: Default::default(),
3689 })));
3690 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3691 left: distance_call_ast,
3692 operator: ast::BinaryOperator::Eq,
3693 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3694 value: ast::LiteralValue::Number {
3695 value: distance.distance.value,
3696 suffix: distance.distance.units,
3697 },
3698 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3699 KclError::refactor(format!(
3700 "Could not format numeric suffix: {:?}",
3701 distance.distance.units
3702 ))
3703 })?,
3704 digest: None,
3705 }))),
3706 digest: None,
3707 })));
3708
3709 let (sketch_block_ref, _) = self.mutate_ast(
3711 new_ast,
3712 sketch_id,
3713 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3714 )?;
3715 Ok(sketch_block_ref)
3716 }
3717
3718 async fn add_angle(
3719 &mut self,
3720 sketch: ObjectId,
3721 angle: Angle,
3722 new_ast: &mut ast::Node<ast::Program>,
3723 ) -> Result<AstNodeRef, KclError> {
3724 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3725 return Err(KclError::refactor(format!(
3726 "Angle constraint must have exactly 2 lines, got {}",
3727 angle.lines.len()
3728 )));
3729 };
3730 let sketch_id = sketch;
3731
3732 let line0_object = self
3734 .scene_graph
3735 .objects
3736 .get(l0_id.0)
3737 .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3738 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3739 return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3740 };
3741 let Segment::Line(_) = line0_segment else {
3742 return Err(KclError::refactor(format!(
3743 "Only lines can be constrained to meet at an angle: {line0_object:?}",
3744 )));
3745 };
3746 let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3747
3748 let line1_object = self
3749 .scene_graph
3750 .objects
3751 .get(l1_id.0)
3752 .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3753 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3754 return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3755 };
3756 let Segment::Line(_) = line1_segment else {
3757 return Err(KclError::refactor(format!(
3758 "Only lines can be constrained to meet at an angle: {line1_object:?}",
3759 )));
3760 };
3761 let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3762
3763 let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3765 callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3766 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3767 ast::ArrayExpression {
3768 elements: vec![l0_ast, l1_ast],
3769 digest: None,
3770 non_code_meta: Default::default(),
3771 },
3772 )))),
3773 arguments: Default::default(),
3774 digest: None,
3775 non_code_meta: Default::default(),
3776 })));
3777 let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3778 left: angle_call_ast,
3779 operator: ast::BinaryOperator::Eq,
3780 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3781 value: ast::LiteralValue::Number {
3782 value: angle.angle.value,
3783 suffix: angle.angle.units,
3784 },
3785 raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3786 KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3787 })?,
3788 digest: None,
3789 }))),
3790 digest: None,
3791 })));
3792
3793 let (sketch_block_ref, _) = self.mutate_ast(
3795 new_ast,
3796 sketch_id,
3797 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3798 )?;
3799 Ok(sketch_block_ref)
3800 }
3801
3802 async fn add_tangent(
3803 &mut self,
3804 sketch: ObjectId,
3805 tangent: Tangent,
3806 new_ast: &mut ast::Node<ast::Program>,
3807 ) -> Result<AstNodeRef, KclError> {
3808 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3809 return Err(KclError::refactor(format!(
3810 "Tangent constraint must have exactly 2 segments, got {}",
3811 tangent.input.len()
3812 )));
3813 };
3814 let sketch_id = sketch;
3815
3816 let seg0_object = self
3817 .scene_graph
3818 .objects
3819 .get(seg0_id.0)
3820 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3821 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3822 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3823 };
3824 let seg0_ast = match seg0_segment {
3825 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3826 self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3827 }
3828 _ => {
3829 return Err(KclError::refactor(format!(
3830 "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3831 )));
3832 }
3833 };
3834
3835 let seg1_object = self
3836 .scene_graph
3837 .objects
3838 .get(seg1_id.0)
3839 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3840 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3841 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3842 };
3843 let seg1_ast = match seg1_segment {
3844 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3845 self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3846 }
3847 _ => {
3848 return Err(KclError::refactor(format!(
3849 "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3850 )));
3851 }
3852 };
3853
3854 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3855 let (sketch_block_ref, _) = self.mutate_ast(
3856 new_ast,
3857 sketch_id,
3858 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3859 )?;
3860 Ok(sketch_block_ref)
3861 }
3862
3863 async fn add_symmetric(
3864 &mut self,
3865 sketch: ObjectId,
3866 symmetric: Symmetric,
3867 new_ast: &mut ast::Node<ast::Program>,
3868 ) -> Result<AstNodeRef, KclError> {
3869 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3870 return Err(KclError::refactor(format!(
3871 "Symmetric constraint must have exactly 2 inputs, got {}",
3872 symmetric.input.len()
3873 )));
3874 };
3875 let sketch_id = sketch;
3876
3877 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3878 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3879 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3880
3881 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3882 let (sketch_block_ref, _) = self.mutate_ast(
3883 new_ast,
3884 sketch_id,
3885 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3886 )?;
3887 Ok(sketch_block_ref)
3888 }
3889
3890 async fn add_midpoint(
3891 &mut self,
3892 sketch: ObjectId,
3893 midpoint: Midpoint,
3894 new_ast: &mut ast::Node<ast::Program>,
3895 ) -> Result<AstNodeRef, KclError> {
3896 let sketch_id = sketch;
3897 let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
3898
3899 let segment_object = self
3900 .scene_graph
3901 .objects
3902 .get(midpoint.segment.0)
3903 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3904 let ObjectKind::Segment {
3905 segment: midpoint_segment,
3906 } = &segment_object.kind
3907 else {
3908 return Err(KclError::refactor(format!(
3909 "Object must be a segment, but it was {}",
3910 segment_object.kind.human_friendly_kind_with_article()
3911 )));
3912 };
3913 let segment_ast = match midpoint_segment {
3914 Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
3915 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3916 _ => {
3917 return Err(KclError::refactor(format!(
3918 "Midpoint target must be a line or arc segment but it was {}",
3919 midpoint_segment.human_friendly_kind_with_article()
3920 )));
3921 }
3922 };
3923
3924 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3925 let (sketch_block_ref, _) = self.mutate_ast(
3926 new_ast,
3927 sketch_id,
3928 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3929 )?;
3930 Ok(sketch_block_ref)
3931 }
3932
3933 async fn add_equal_radius(
3934 &mut self,
3935 sketch: ObjectId,
3936 equal_radius: EqualRadius,
3937 new_ast: &mut ast::Node<ast::Program>,
3938 ) -> Result<AstNodeRef, KclError> {
3939 if equal_radius.input.len() < 2 {
3940 return Err(KclError::refactor(format!(
3941 "equalRadius constraint must have at least 2 segments, got {}",
3942 equal_radius.input.len()
3943 )));
3944 }
3945
3946 let sketch_id = sketch;
3947 let input_asts = equal_radius
3948 .input
3949 .iter()
3950 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3951 .collect::<Result<Vec<_>, _>>()?;
3952
3953 let equal_radius_ast = create_equal_radius_ast(input_asts);
3954 let (sketch_block_ref, _) = self.mutate_ast(
3955 new_ast,
3956 sketch_id,
3957 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3958 )?;
3959 Ok(sketch_block_ref)
3960 }
3961
3962 async fn add_radius(
3963 &mut self,
3964 sketch: ObjectId,
3965 radius: Radius,
3966 new_ast: &mut ast::Node<ast::Program>,
3967 ) -> Result<AstNodeRef, KclError> {
3968 let params = ArcSizeConstraintParams {
3969 points: vec![radius.arc],
3970 function_name: RADIUS_FN,
3971 value: radius.radius.value,
3972 units: radius.radius.units,
3973 label_position: radius.label_position,
3974 constraint_type_name: "Radius",
3975 };
3976 self.add_arc_size_constraint(sketch, params, new_ast).await
3977 }
3978
3979 async fn add_diameter(
3980 &mut self,
3981 sketch: ObjectId,
3982 diameter: Diameter,
3983 new_ast: &mut ast::Node<ast::Program>,
3984 ) -> Result<AstNodeRef, KclError> {
3985 let params = ArcSizeConstraintParams {
3986 points: vec![diameter.arc],
3987 function_name: DIAMETER_FN,
3988 value: diameter.diameter.value,
3989 units: diameter.diameter.units,
3990 label_position: diameter.label_position,
3991 constraint_type_name: "Diameter",
3992 };
3993 self.add_arc_size_constraint(sketch, params, new_ast).await
3994 }
3995
3996 async fn add_fixed_constraints(
3997 &mut self,
3998 sketch: ObjectId,
3999 points: Vec<FixedPoint>,
4000 new_ast: &mut ast::Node<ast::Program>,
4001 ) -> Result<AstNodeRef, KclError> {
4002 let mut sketch_block_ref = None;
4003
4004 for fixed_point in points {
4005 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4006 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4007 .map_err(|err| KclError::refactor(err.to_string()))?;
4008
4009 let (sketch_ref, _) = self.mutate_ast(
4010 new_ast,
4011 sketch,
4012 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4013 )?;
4014 sketch_block_ref = Some(sketch_ref);
4015 }
4016
4017 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4018 }
4019
4020 async fn add_arc_size_constraint(
4021 &mut self,
4022 sketch: ObjectId,
4023 params: ArcSizeConstraintParams,
4024 new_ast: &mut ast::Node<ast::Program>,
4025 ) -> Result<AstNodeRef, KclError> {
4026 let sketch_id = sketch;
4027
4028 if params.points.len() != 1 {
4030 return Err(KclError::refactor(format!(
4031 "{} constraint must have exactly 1 argument (an arc segment), got {}",
4032 params.constraint_type_name,
4033 params.points.len()
4034 )));
4035 }
4036
4037 let arc_id = params.points[0];
4038 let arc_object = self
4039 .scene_graph
4040 .objects
4041 .get(arc_id.0)
4042 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4043 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4044 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4045 };
4046 let ref_type = match arc_segment {
4047 Segment::Arc(_) => ARC_VARIABLE,
4048 Segment::Circle(_) => CIRCLE_VARIABLE,
4049 _ => {
4050 return Err(KclError::refactor(format!(
4051 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4052 params.constraint_type_name
4053 )));
4054 }
4055 };
4056 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4058 let arguments = match ¶ms.label_position {
4059 Some(label_position) => vec![ast::LabeledArg {
4060 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4061 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4062 }],
4063 None => Default::default(),
4064 };
4065
4066 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4068 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4069 unlabeled: Some(arc_ast),
4070 arguments,
4071 digest: None,
4072 non_code_meta: Default::default(),
4073 })));
4074 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4075 left: call_ast,
4076 operator: ast::BinaryOperator::Eq,
4077 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4078 value: ast::LiteralValue::Number {
4079 value: params.value,
4080 suffix: params.units,
4081 },
4082 raw: format_number_literal(params.value, params.units, None)
4083 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4084 digest: None,
4085 }))),
4086 digest: None,
4087 })));
4088
4089 let (sketch_block_ref, _) = self.mutate_ast(
4091 new_ast,
4092 sketch_id,
4093 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4094 )?;
4095 Ok(sketch_block_ref)
4096 }
4097
4098 async fn add_horizontal_distance(
4099 &mut self,
4100 sketch: ObjectId,
4101 distance: Distance,
4102 new_ast: &mut ast::Node<ast::Program>,
4103 ) -> Result<AstNodeRef, KclError> {
4104 let sketch_id = sketch;
4105 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4106 [pt0, pt1] => [
4107 self.coincident_segment_to_ast(pt0, new_ast)?,
4108 self.coincident_segment_to_ast(pt1, new_ast)?,
4109 ],
4110 _ => {
4111 return Err(KclError::refactor(format!(
4112 "Horizontal distance constraint must have exactly 2 points, got {}",
4113 distance.points.len()
4114 )));
4115 }
4116 };
4117
4118 let arguments = match &distance.label_position {
4119 Some(label_position) => vec![ast::LabeledArg {
4120 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4121 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4122 }],
4123 None => Default::default(),
4124 };
4125
4126 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4128 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
4129 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4130 ast::ArrayExpression {
4131 elements: vec![pt0_ast, pt1_ast],
4132 digest: None,
4133 non_code_meta: Default::default(),
4134 },
4135 )))),
4136 arguments,
4137 digest: None,
4138 non_code_meta: Default::default(),
4139 })));
4140 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4141 left: distance_call_ast,
4142 operator: ast::BinaryOperator::Eq,
4143 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4144 value: ast::LiteralValue::Number {
4145 value: distance.distance.value,
4146 suffix: distance.distance.units,
4147 },
4148 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4149 KclError::refactor(format!(
4150 "Could not format numeric suffix: {:?}",
4151 distance.distance.units
4152 ))
4153 })?,
4154 digest: None,
4155 }))),
4156 digest: None,
4157 })));
4158
4159 let (sketch_block_ref, _) = self.mutate_ast(
4161 new_ast,
4162 sketch_id,
4163 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4164 )?;
4165 Ok(sketch_block_ref)
4166 }
4167
4168 async fn add_vertical_distance(
4169 &mut self,
4170 sketch: ObjectId,
4171 distance: Distance,
4172 new_ast: &mut ast::Node<ast::Program>,
4173 ) -> Result<AstNodeRef, KclError> {
4174 let sketch_id = sketch;
4175 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4176 [pt0, pt1] => [
4177 self.coincident_segment_to_ast(pt0, new_ast)?,
4178 self.coincident_segment_to_ast(pt1, new_ast)?,
4179 ],
4180 _ => {
4181 return Err(KclError::refactor(format!(
4182 "Vertical distance constraint must have exactly 2 points, got {}",
4183 distance.points.len()
4184 )));
4185 }
4186 };
4187
4188 let arguments = match &distance.label_position {
4189 Some(label_position) => vec![ast::LabeledArg {
4190 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4191 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4192 }],
4193 None => Default::default(),
4194 };
4195
4196 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4198 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
4199 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4200 ast::ArrayExpression {
4201 elements: vec![pt0_ast, pt1_ast],
4202 digest: None,
4203 non_code_meta: Default::default(),
4204 },
4205 )))),
4206 arguments,
4207 digest: None,
4208 non_code_meta: Default::default(),
4209 })));
4210 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4211 left: distance_call_ast,
4212 operator: ast::BinaryOperator::Eq,
4213 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4214 value: ast::LiteralValue::Number {
4215 value: distance.distance.value,
4216 suffix: distance.distance.units,
4217 },
4218 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4219 KclError::refactor(format!(
4220 "Could not format numeric suffix: {:?}",
4221 distance.distance.units
4222 ))
4223 })?,
4224 digest: None,
4225 }))),
4226 digest: None,
4227 })));
4228
4229 let (sketch_block_ref, _) = self.mutate_ast(
4231 new_ast,
4232 sketch_id,
4233 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4234 )?;
4235 Ok(sketch_block_ref)
4236 }
4237
4238 async fn add_horizontal(
4239 &mut self,
4240 sketch: ObjectId,
4241 horizontal: Horizontal,
4242 new_ast: &mut ast::Node<ast::Program>,
4243 ) -> Result<AstNodeRef, KclError> {
4244 let sketch_id = sketch;
4245
4246 let first_arg_ast = match horizontal {
4248 Horizontal::Line { line } => {
4249 let line_object = self
4250 .scene_graph
4251 .objects
4252 .get(line.0)
4253 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4254 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4255 let kind = line_object.kind.human_friendly_kind_with_article();
4256 return Err(KclError::refactor(format!(
4257 "This constraint only works on Segments, but you selected {kind}"
4258 )));
4259 };
4260 let Segment::Line(_) = line_segment else {
4261 return Err(KclError::refactor(format!(
4262 "Only lines can be made horizontal, but you selected {}",
4263 line_segment.human_friendly_kind_with_article(),
4264 )));
4265 };
4266 self.line_id_to_ast_reference(line, new_ast)?
4267 }
4268 Horizontal::Points { points } => {
4269 let point_asts = points
4270 .iter()
4271 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4272 .collect::<Result<Vec<_>, _>>()?;
4273 ast::ArrayExpression::new(point_asts).into()
4274 }
4275 };
4276 let horizontal_ast = create_horizontal_ast(first_arg_ast);
4278
4279 let (sketch_block_ref, _) = self.mutate_ast(
4281 new_ast,
4282 sketch_id,
4283 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4284 )?;
4285 Ok(sketch_block_ref)
4286 }
4287
4288 async fn add_lines_equal_length(
4289 &mut self,
4290 sketch: ObjectId,
4291 lines_equal_length: LinesEqualLength,
4292 new_ast: &mut ast::Node<ast::Program>,
4293 ) -> Result<AstNodeRef, KclError> {
4294 if lines_equal_length.lines.len() < 2 {
4295 return Err(KclError::refactor(format!(
4296 "Lines equal length constraint must have at least 2 lines, got {}",
4297 lines_equal_length.lines.len()
4298 )));
4299 };
4300
4301 let sketch_id = sketch;
4302
4303 let line_asts = lines_equal_length
4305 .lines
4306 .iter()
4307 .map(|line_id| {
4308 let line_object = self
4309 .scene_graph
4310 .objects
4311 .get(line_id.0)
4312 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4313 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4314 let kind = line_object.kind.human_friendly_kind_with_article();
4315 return Err(KclError::refactor(format!(
4316 "This constraint only works on Segments, but you selected {kind}"
4317 )));
4318 };
4319 let Segment::Line(_) = line_segment else {
4320 let kind = line_segment.human_friendly_kind_with_article();
4321 return Err(KclError::refactor(format!(
4322 "Only lines can be made equal length, but you selected {kind}"
4323 )));
4324 };
4325
4326 self.line_id_to_ast_reference(*line_id, new_ast)
4327 })
4328 .collect::<Result<Vec<_>, _>>()?;
4329
4330 let equal_length_ast = create_equal_length_ast(line_asts);
4332
4333 let (sketch_block_ref, _) = self.mutate_ast(
4335 new_ast,
4336 sketch_id,
4337 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4338 )?;
4339 Ok(sketch_block_ref)
4340 }
4341
4342 fn equal_radius_segment_id_to_ast_reference(
4343 &mut self,
4344 segment_id: ObjectId,
4345 new_ast: &mut ast::Node<ast::Program>,
4346 ) -> Result<ast::Expr, KclError> {
4347 let segment_object = self
4348 .scene_graph
4349 .objects
4350 .get(segment_id.0)
4351 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4352 let ObjectKind::Segment { segment } = &segment_object.kind else {
4353 return Err(KclError::refactor(format!(
4354 "Object is not a segment, it was {}",
4355 segment_object.kind.human_friendly_kind_with_article()
4356 )));
4357 };
4358
4359 let ref_type = match segment {
4360 Segment::Arc(_) => ARC_VARIABLE,
4361 Segment::Circle(_) => CIRCLE_VARIABLE,
4362 _ => {
4363 return Err(KclError::refactor(format!(
4364 "equalRadius supports only arc/circle segments, got {}",
4365 segment.human_friendly_kind_with_article()
4366 )));
4367 }
4368 };
4369
4370 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4371 }
4372
4373 fn symmetric_input_id_to_ast_reference(
4374 &mut self,
4375 segment_id: ObjectId,
4376 new_ast: &mut ast::Node<ast::Program>,
4377 ) -> Result<ast::Expr, KclError> {
4378 let segment_object = self
4379 .scene_graph
4380 .objects
4381 .get(segment_id.0)
4382 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4383 let ObjectKind::Segment { segment } = &segment_object.kind else {
4384 return Err(KclError::refactor(format!(
4385 "Object is not a segment, it was {}",
4386 segment_object.kind.human_friendly_kind_with_article()
4387 )));
4388 };
4389
4390 match segment {
4391 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4392 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4393 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4394 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4395 Segment::ControlPointSpline(_) => Err(KclError::refactor(
4396 "Symmetric does not yet support control point splines".to_owned(),
4397 )),
4398 }
4399 }
4400
4401 fn symmetric_axis_id_to_ast_reference(
4402 &mut self,
4403 segment_id: ObjectId,
4404 new_ast: &mut ast::Node<ast::Program>,
4405 ) -> Result<ast::Expr, KclError> {
4406 let segment_object = self
4407 .scene_graph
4408 .objects
4409 .get(segment_id.0)
4410 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4411 let ObjectKind::Segment { segment } = &segment_object.kind else {
4412 return Err(KclError::refactor(format!(
4413 "Object is not a segment, it was {}",
4414 segment_object.kind.human_friendly_kind_with_article()
4415 )));
4416 };
4417 match segment {
4418 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4419 _ => Err(KclError::refactor(format!(
4420 "Symmetric axis must be a line, got {}",
4421 segment.human_friendly_kind_with_article()
4422 ))),
4423 }
4424 }
4425
4426 async fn add_parallel(
4427 &mut self,
4428 sketch: ObjectId,
4429 parallel: Parallel,
4430 new_ast: &mut ast::Node<ast::Program>,
4431 ) -> Result<AstNodeRef, KclError> {
4432 if parallel.lines.len() < 2 {
4433 return Err(KclError::refactor(format!(
4434 "Parallel constraint must have at least 2 lines, got {}",
4435 parallel.lines.len()
4436 )));
4437 };
4438
4439 let sketch_id = sketch;
4440
4441 let line_asts = parallel
4442 .lines
4443 .iter()
4444 .map(|line_id| {
4445 let line_object = self
4446 .scene_graph
4447 .objects
4448 .get(line_id.0)
4449 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4450 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4451 let kind = line_object.kind.human_friendly_kind_with_article();
4452 return Err(KclError::refactor(format!(
4453 "This constraint only works on Segments, but you selected {kind}"
4454 )));
4455 };
4456 let Segment::Line(_) = line_segment else {
4457 let kind = line_segment.human_friendly_kind_with_article();
4458 return Err(KclError::refactor(format!(
4459 "Only lines can be made parallel, but you selected {kind}"
4460 )));
4461 };
4462
4463 self.line_id_to_ast_reference(*line_id, new_ast)
4464 })
4465 .collect::<Result<Vec<_>, _>>()?;
4466
4467 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4468 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4469 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4470 ast::ArrayExpression {
4471 elements: line_asts,
4472 digest: None,
4473 non_code_meta: Default::default(),
4474 },
4475 )))),
4476 arguments: Default::default(),
4477 digest: None,
4478 non_code_meta: Default::default(),
4479 })));
4480
4481 let (sketch_block_ref, _) = self.mutate_ast(
4482 new_ast,
4483 sketch_id,
4484 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4485 )?;
4486 Ok(sketch_block_ref)
4487 }
4488
4489 async fn add_perpendicular(
4490 &mut self,
4491 sketch: ObjectId,
4492 perpendicular: Perpendicular,
4493 new_ast: &mut ast::Node<ast::Program>,
4494 ) -> Result<AstNodeRef, KclError> {
4495 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4496 .await
4497 }
4498
4499 async fn add_lines_at_angle_constraint(
4500 &mut self,
4501 sketch: ObjectId,
4502 angle_kind: LinesAtAngleKind,
4503 lines: Vec<ObjectId>,
4504 new_ast: &mut ast::Node<ast::Program>,
4505 ) -> Result<AstNodeRef, KclError> {
4506 let &[line0_id, line1_id] = lines.as_slice() else {
4507 return Err(KclError::refactor(format!(
4508 "{} constraint must have exactly 2 lines, got {}",
4509 angle_kind.to_function_name(),
4510 lines.len()
4511 )));
4512 };
4513
4514 let sketch_id = sketch;
4515
4516 let line0_object = self
4518 .scene_graph
4519 .objects
4520 .get(line0_id.0)
4521 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4522 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4523 let kind = line0_object.kind.human_friendly_kind_with_article();
4524 return Err(KclError::refactor(format!(
4525 "This constraint only works on Segments, but you selected {kind}"
4526 )));
4527 };
4528 let Segment::Line(_) = line0_segment else {
4529 return Err(KclError::refactor(format!(
4530 "Only lines can be made {}, but you selected {}",
4531 angle_kind.to_function_name(),
4532 line0_segment.human_friendly_kind_with_article(),
4533 )));
4534 };
4535 let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4536
4537 let line1_object = self
4538 .scene_graph
4539 .objects
4540 .get(line1_id.0)
4541 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4542 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4543 let kind = line1_object.kind.human_friendly_kind_with_article();
4544 return Err(KclError::refactor(format!(
4545 "This constraint only works on Segments, but you selected {kind}"
4546 )));
4547 };
4548 let Segment::Line(_) = line1_segment else {
4549 return Err(KclError::refactor(format!(
4550 "Only lines can be made {}, but you selected {}",
4551 angle_kind.to_function_name(),
4552 line1_segment.human_friendly_kind_with_article(),
4553 )));
4554 };
4555 let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4556
4557 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4559 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4560 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4561 ast::ArrayExpression {
4562 elements: vec![line0_ast, line1_ast],
4563 digest: None,
4564 non_code_meta: Default::default(),
4565 },
4566 )))),
4567 arguments: Default::default(),
4568 digest: None,
4569 non_code_meta: Default::default(),
4570 })));
4571
4572 let (sketch_block_ref, _) = self.mutate_ast(
4574 new_ast,
4575 sketch_id,
4576 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4577 )?;
4578 Ok(sketch_block_ref)
4579 }
4580
4581 async fn add_vertical(
4582 &mut self,
4583 sketch: ObjectId,
4584 vertical: Vertical,
4585 new_ast: &mut ast::Node<ast::Program>,
4586 ) -> Result<AstNodeRef, KclError> {
4587 let sketch_id = sketch;
4588
4589 let first_arg_ast = match vertical {
4590 Vertical::Line { line } => {
4591 let line_object = self
4593 .scene_graph
4594 .objects
4595 .get(line.0)
4596 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4597 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4598 let kind = line_object.kind.human_friendly_kind_with_article();
4599 return Err(KclError::refactor(format!(
4600 "This constraint only works on Segments, but you selected {kind}"
4601 )));
4602 };
4603 let Segment::Line(_) = line_segment else {
4604 return Err(KclError::refactor(format!(
4605 "Only lines can be made vertical, but you selected {}",
4606 line_segment.human_friendly_kind_with_article()
4607 )));
4608 };
4609 self.line_id_to_ast_reference(line, new_ast)?
4610 }
4611 Vertical::Points { points } => {
4612 let point_asts = points
4613 .iter()
4614 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4615 .collect::<Result<Vec<_>, _>>()?;
4616 ast::ArrayExpression::new(point_asts).into()
4617 }
4618 };
4619 let vertical_ast = create_vertical_ast(first_arg_ast);
4621
4622 let (sketch_block_ref, _) = self.mutate_ast(
4624 new_ast,
4625 sketch_id,
4626 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4627 )?;
4628 Ok(sketch_block_ref)
4629 }
4630
4631 async fn execute_after_add_constraint(
4632 &mut self,
4633 ctx: &ExecutorContext,
4634 sketch_id: ObjectId,
4635 sketch_block_ref: AstNodeRef,
4636 new_ast: &mut ast::Node<ast::Program>,
4637 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4638 let new_source = source_from_ast(new_ast);
4640 let (new_program, errors) = Program::parse(&new_source)
4642 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4643 if !errors.is_empty() {
4644 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4645 "Error parsing KCL source after adding constraint: {errors:?}"
4646 ))));
4647 }
4648 let Some(new_program) = new_program else {
4649 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4650 "No AST produced after adding constraint".to_string(),
4651 )));
4652 };
4653 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4654 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4655 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4656 )))
4657 })?;
4658
4659 let mut truncated_program = new_program.clone();
4662 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4663 .map_err(KclErrorWithOutputs::no_outputs)?;
4664
4665 let outcome = ctx
4667 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4668 .await?;
4669
4670 let new_object_ids = {
4671 let constraint_id = outcome
4673 .source_range_to_object
4674 .get(&constraint_node_ref.range)
4675 .copied()
4676 .ok_or_else(|| {
4677 KclErrorWithOutputs::from_error_outcome(
4678 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4679 outcome.clone(),
4680 )
4681 })?;
4682 vec![constraint_id]
4683 };
4684
4685 self.program = new_program;
4688
4689 let outcome = self.update_state_after_exec(outcome, true);
4691
4692 let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4693 let scene_graph_delta = SceneGraphDelta {
4694 new_graph: self.scene_graph_for_ui(),
4695 invalidates_ids: false,
4696 new_objects: new_object_ids,
4697 exec_outcome: outcome,
4698 };
4699 Ok((src_delta, scene_graph_delta))
4700 }
4701
4702 fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4703 let commit_failure = || {
4704 KclErrorWithOutputs::from_error_outcome(
4705 KclError::refactor(format!("Could not update KCL after {operation}.")),
4706 outcome.clone(),
4707 )
4708 };
4709
4710 let default_length_unit = self.default_length_unit();
4711 let mut settled_ast = self.program.ast.clone();
4712 let mut committed_solver_value = false;
4713 for (var_range, node_path, value) in &outcome.var_solutions {
4714 let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4715 return Err(commit_failure());
4716 };
4717 let new_value = match &lookup {
4718 Some(current_literal) => {
4719 if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4720 continue;
4721 }
4722 preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4723 }
4724 None => {
4725 Number {
4729 value: number_value_in_default_length_units(*value, default_length_unit),
4730 units: default_length_unit.into(),
4731 }
4732 }
4733 };
4734 committed_solver_value = true;
4735 let source_ref = SourceRef::Simple {
4736 range: *var_range,
4737 node_path: node_path.clone(),
4738 };
4739 mutate_ast_node_by_source_ref(
4740 &mut settled_ast,
4741 &source_ref,
4742 AstMutateCommand::EditVarInitialValue { value: new_value },
4743 )
4744 .map_err(|_| commit_failure())?;
4745 }
4746
4747 if !committed_solver_value {
4748 return Ok(SourceDelta {
4749 text: self.program.original_file_contents.clone(),
4750 });
4751 }
4752
4753 let settled_source = source_from_ast(&settled_ast);
4754 let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4755 if !errors.is_empty() {
4756 return Err(commit_failure());
4757 }
4758 let Some(settled_program) = settled_program else {
4759 return Err(commit_failure());
4760 };
4761
4762 self.program = settled_program;
4763
4764 Ok(SourceDelta { text: settled_source })
4765 }
4766
4767 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4769 if segment_ids_set.contains(&segment_id) {
4770 return true;
4771 }
4772
4773 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4774 return false;
4775 };
4776 let ObjectKind::Segment { segment } = &segment_object.kind else {
4777 return false;
4778 };
4779 let Segment::Point(point) = segment else {
4780 return false;
4781 };
4782
4783 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4784 }
4785
4786 fn remaining_constraint_segments(
4787 &self,
4788 segments: &[ConstraintSegment],
4789 segment_ids_set: &AhashIndexSet<ObjectId>,
4790 ) -> Vec<ConstraintSegment> {
4791 segments
4792 .iter()
4793 .copied()
4794 .filter(|segment| match segment {
4795 ConstraintSegment::Origin(_) => true,
4796 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4797 })
4798 .collect()
4799 }
4800
4801 fn find_referenced_constraints(
4802 &self,
4803 sketch_id: ObjectId,
4804 segment_ids_set: &AhashIndexSet<ObjectId>,
4805 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4806 let sketch_object = self
4808 .scene_graph
4809 .objects
4810 .get(sketch_id.0)
4811 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4812 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4813 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4814 };
4815 let segment_or_owner_matches = |segment_id: ObjectId| {
4816 if segment_ids_set.contains(&segment_id) {
4817 return true;
4818 }
4819 let segment_object = self.scene_graph.objects.get(segment_id.0);
4820 if let Some(obj) = segment_object
4821 && let ObjectKind::Segment { segment } = &obj.kind
4822 {
4823 match segment {
4824 Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4825 Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4826 _ => false,
4827 }
4828 } else {
4829 false
4830 }
4831 };
4832 let mut constraint_ids_set = AhashIndexSet::default();
4833 for constraint_id in &sketch.constraints {
4834 let constraint_object = self
4835 .scene_graph
4836 .objects
4837 .get(constraint_id.0)
4838 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4839 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4840 return Err(KclError::refactor(format!(
4841 "Object is not a constraint, it is {}",
4842 constraint_object.kind.human_friendly_kind_with_article()
4843 )));
4844 };
4845 let depends_on_segment = match constraint {
4846 Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4847 Constraint::Distance(d) => d.point_ids().any(segment_or_owner_matches),
4848 Constraint::Fixed(fixed) => fixed
4849 .points
4850 .iter()
4851 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4852 Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4853 Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4854 Constraint::EqualRadius(equal_radius) => {
4855 equal_radius.input.iter().copied().any(segment_or_owner_matches)
4856 }
4857 Constraint::HorizontalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4858 Constraint::VerticalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4859 Constraint::Horizontal(h) => match h {
4860 Horizontal::Line { line } => segment_or_owner_matches(*line),
4861 Horizontal::Points { points } => points.iter().any(|point| match point {
4862 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4863 ConstraintSegment::Origin(_) => false,
4864 }),
4865 },
4866 Constraint::Vertical(v) => match v {
4867 Vertical::Line { line } => segment_or_owner_matches(*line),
4868 Vertical::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::LinesEqualLength(lines_equal_length) => {
4874 lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4875 }
4876 Constraint::Midpoint(midpoint) => {
4877 segment_or_owner_matches(midpoint.segment)
4878 || matches!(
4879 midpoint.point,
4880 ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4881 )
4882 }
4883 Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4884 Constraint::Perpendicular(perpendicular) => {
4885 perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4886 }
4887 Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4888 Constraint::Symmetric(symmetric) => {
4889 segment_or_owner_matches(symmetric.axis)
4890 || symmetric.input.iter().copied().any(segment_or_owner_matches)
4891 }
4892 Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4893 };
4894 if depends_on_segment {
4895 constraint_ids_set.insert(*constraint_id);
4896 }
4897 }
4898 Ok(constraint_ids_set)
4899 }
4900
4901 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4902 let mut outcome = outcome;
4903 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4904
4905 if freedom_analysis_ran {
4906 self.point_freedom_cache.clear();
4909 for new_obj in &new_objects {
4910 if let ObjectKind::Segment {
4911 segment: crate::front::Segment::Point(point),
4912 } = &new_obj.kind
4913 {
4914 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4915 }
4916 }
4917 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4918 self.scene_graph.objects = new_objects;
4920 } else {
4921 for old_obj in &self.scene_graph.objects {
4924 if let ObjectKind::Segment {
4925 segment: crate::front::Segment::Point(point),
4926 } = &old_obj.kind
4927 {
4928 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4929 }
4930 }
4931
4932 let mut updated_objects = Vec::with_capacity(new_objects.len());
4934 for new_obj in new_objects {
4935 let mut obj = new_obj;
4936 if let ObjectKind::Segment {
4937 segment: crate::front::Segment::Point(point),
4938 } = &mut obj.kind
4939 {
4940 let new_freedom = point.freedom;
4941 match new_freedom {
4947 Freedom::Free => {
4948 match self.point_freedom_cache.get(&obj.id).copied() {
4949 Some(Freedom::Conflict) => {
4950 }
4953 Some(Freedom::Fixed) => {
4954 point.freedom = Freedom::Fixed;
4956 }
4957 Some(Freedom::Free) => {
4958 }
4960 None => {
4961 }
4963 }
4964 }
4965 Freedom::Fixed => {
4966 }
4968 Freedom::Conflict => {
4969 }
4971 }
4972 self.point_freedom_cache.insert(obj.id, point.freedom);
4974 }
4975 updated_objects.push(obj);
4976 }
4977
4978 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4979 self.scene_graph.objects = updated_objects;
4980 }
4981 outcome
4982 }
4983
4984 fn mutate_ast(
4985 &mut self,
4986 ast: &mut ast::Node<ast::Program>,
4987 object_id: ObjectId,
4988 command: AstMutateCommand,
4989 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4990 let sketch_object = self
4991 .scene_graph
4992 .objects
4993 .get(object_id.0)
4994 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4995 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4996 }
4997}
4998
4999fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5000 let sketch_object = scene_graph
5002 .objects
5003 .get(sketch_id.0)
5004 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5005 let ObjectKind::Sketch(_) = &sketch_object.kind else {
5006 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5007 };
5008 expect_single_node_ref(sketch_object)
5009}
5010
5011fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5012 match &object.source {
5013 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5014 range: *range,
5015 node_path: node_path.clone(),
5016 }),
5017 SourceRef::BackTrace { ranges } => {
5018 let [range] = ranges.as_slice() else {
5019 return Err(KclError::refactor(format!(
5020 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5021 ranges.len()
5022 )));
5023 };
5024 Ok(AstNodeRef {
5025 range: range.0,
5026 node_path: range.1.clone(),
5027 })
5028 }
5029 }
5030}
5031
5032fn only_sketch_block_from_range(
5035 ast: &mut ast::Node<ast::Program>,
5036 sketch_block_range: SourceRange,
5037 edit_kind: ChangeKind,
5038) -> Result<(), KclError> {
5039 let r1 = sketch_block_range;
5040 let matches_range = |r2: SourceRange| -> bool {
5041 match edit_kind {
5044 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5045 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5047 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5048 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5050 }
5051 };
5052 let mut found = false;
5053 for item in ast.body.iter_mut() {
5054 match item {
5055 ast::BodyItem::ImportStatement(_) => {}
5056 ast::BodyItem::ExpressionStatement(node) => {
5057 if matches_range(SourceRange::from(&*node))
5058 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5059 {
5060 sketch_block.is_being_edited = true;
5061 found = true;
5062 break;
5063 }
5064 }
5065 ast::BodyItem::VariableDeclaration(node) => {
5066 if matches_range(SourceRange::from(&node.declaration.init))
5067 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5068 {
5069 sketch_block.is_being_edited = true;
5070 found = true;
5071 break;
5072 }
5073 }
5074 ast::BodyItem::TypeDeclaration(_) => {}
5075 ast::BodyItem::ReturnStatement(node) => {
5076 if matches_range(SourceRange::from(&node.argument))
5077 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5078 {
5079 sketch_block.is_being_edited = true;
5080 found = true;
5081 break;
5082 }
5083 }
5084 }
5085 }
5086 if !found {
5087 return Err(KclError::refactor(format!(
5088 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5089 )));
5090 }
5091
5092 Ok(())
5093}
5094
5095fn only_sketch_block(
5096 ast: &mut ast::Node<ast::Program>,
5097 sketch_block_ref: &AstNodeRef,
5098 edit_kind: ChangeKind,
5099) -> Result<(), KclError> {
5100 let Some(target_node_path) = &sketch_block_ref.node_path else {
5101 #[cfg(target_arch = "wasm32")]
5102 web_sys::console::warn_1(
5103 &format!(
5104 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5105 &sketch_block_ref
5106 )
5107 .into(),
5108 );
5109 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5110 };
5111 let mut found = false;
5112 for item in ast.body.iter_mut() {
5113 match item {
5114 ast::BodyItem::ImportStatement(_) => {}
5115 ast::BodyItem::ExpressionStatement(node) => {
5116 if let Some(node_path) = &node.node_path
5118 && node_path == target_node_path
5119 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5120 {
5121 sketch_block.is_being_edited = true;
5122 found = true;
5123 break;
5124 }
5125 if let Some(node_path) = node.expression.node_path()
5127 && node_path == target_node_path
5128 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5129 {
5130 sketch_block.is_being_edited = true;
5131 found = true;
5132 break;
5133 }
5134 }
5135 ast::BodyItem::VariableDeclaration(node) => {
5136 if let Some(node_path) = node.declaration.init.node_path()
5137 && node_path == target_node_path
5138 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5139 {
5140 sketch_block.is_being_edited = true;
5141 found = true;
5142 break;
5143 }
5144 }
5145 ast::BodyItem::TypeDeclaration(_) => {}
5146 ast::BodyItem::ReturnStatement(node) => {
5147 if let Some(node_path) = node.argument.node_path()
5148 && node_path == target_node_path
5149 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5150 {
5151 sketch_block.is_being_edited = true;
5152 found = true;
5153 break;
5154 }
5155 }
5156 }
5157 }
5158 if !found {
5159 return Err(KclError::refactor(format!(
5160 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5161 )));
5162 }
5163
5164 Ok(())
5165}
5166
5167fn sketch_on_ast_expr(
5168 ast: &mut ast::Node<ast::Program>,
5169 scene_graph: &SceneGraph,
5170 on: &Plane,
5171) -> Result<ast::Expr, KclError> {
5172 match on {
5173 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5174 Plane::Object(object_id) => {
5175 let on_object = scene_graph
5176 .objects
5177 .get(object_id.0)
5178 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5179 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5180 return Ok(face_expr);
5181 }
5182 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5183 }
5184 }
5185}
5186
5187fn sketch_face_of_scene_object_ast_expr(
5188 ast: &mut ast::Node<ast::Program>,
5189 on_object: &crate::front::Object,
5190) -> Result<Option<ast::Expr>, KclError> {
5191 let SourceRef::BackTrace { ranges } = &on_object.source else {
5192 return Ok(None);
5193 };
5194
5195 match &on_object.kind {
5196 ObjectKind::Wall(_) => {
5197 let [sweep_range, segment_range] = ranges.as_slice() else {
5198 return Err(KclError::refactor(format!(
5199 "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
5200 ranges.len(),
5201 on_object.artifact_id
5202 )));
5203 };
5204 let sweep_ref = get_or_insert_ast_reference(
5205 ast,
5206 &SourceRef::Simple {
5207 range: sweep_range.0,
5208 node_path: sweep_range.1.clone(),
5209 },
5210 "solid",
5211 None,
5212 )?;
5213 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
5214 return Err(KclError::refactor(format!(
5215 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5216 on_object.artifact_id
5217 )));
5218 };
5219 let solid_name = solid_name_expr.name.name.clone();
5220 let solid_expr = ast_name_expr(solid_name.clone());
5221 let segment_ref = get_or_insert_ast_reference(
5222 ast,
5223 &SourceRef::Simple {
5224 range: segment_range.0,
5225 node_path: segment_range.1.clone(),
5226 },
5227 LINE_VARIABLE,
5228 None,
5229 )?;
5230
5231 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
5232 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5233 return Err(KclError::refactor(format!(
5234 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5235 on_object.artifact_id
5236 )));
5237 };
5238 create_member_expression(
5239 create_member_expression(ast_name_expr(region_name), "tags"),
5240 &segment_name_expr.name.name,
5241 )
5242 } else {
5243 segment_ref
5244 };
5245
5246 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5247 }
5248 ObjectKind::Cap(cap) => {
5249 let [range] = ranges.as_slice() else {
5250 return Err(KclError::refactor(format!(
5251 "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
5252 ranges.len(),
5253 on_object.artifact_id
5254 )));
5255 };
5256 let sweep_ref = get_or_insert_ast_reference(
5257 ast,
5258 &SourceRef::Simple {
5259 range: range.0,
5260 node_path: range.1.clone(),
5261 },
5262 "solid",
5263 None,
5264 )?;
5265 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
5266 return Err(KclError::refactor(format!(
5267 "Could not resolve sweep reference for selected cap: artifact_id={:?}",
5268 on_object.artifact_id
5269 )));
5270 };
5271 let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
5272 let face_expr = match cap.kind {
5274 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5275 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5276 };
5277
5278 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5279 }
5280 _ => Ok(None),
5281 }
5282}
5283
5284fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5285 let mut existing_artifact_ids = scene_objects
5286 .iter()
5287 .map(|object| object.artifact_id)
5288 .collect::<HashSet<_>>();
5289
5290 for artifact in artifact_graph.values() {
5291 match artifact {
5292 Artifact::Wall(wall) => {
5293 if existing_artifact_ids.contains(&wall.id) {
5294 continue;
5295 }
5296
5297 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5298 Artifact::Segment(segment) => Some(segment),
5299 _ => None,
5300 }) else {
5301 continue;
5302 };
5303 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5304 Artifact::Sweep(sweep) => Some(sweep),
5305 _ => None,
5306 }) else {
5307 continue;
5308 };
5309 let source_segment = segment
5310 .original_seg_id
5311 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5312 .and_then(|artifact| match artifact {
5313 Artifact::Segment(segment) => Some(segment),
5314 _ => None,
5315 })
5316 .unwrap_or(segment);
5317 let id = ObjectId(scene_objects.len());
5318 scene_objects.push(crate::front::Object {
5319 id,
5320 kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
5321 label: Default::default(),
5322 comments: Default::default(),
5323 artifact_id: wall.id,
5324 source: SourceRef::BackTrace {
5325 ranges: vec![
5326 (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
5327 (
5328 source_segment.code_ref.range,
5329 Some(source_segment.code_ref.node_path.clone()),
5330 ),
5331 ],
5332 },
5333 });
5334 existing_artifact_ids.insert(wall.id);
5335 }
5336 Artifact::Cap(cap) => {
5337 if existing_artifact_ids.contains(&cap.id) {
5338 continue;
5339 }
5340
5341 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5342 Artifact::Sweep(sweep) => Some(sweep),
5343 _ => None,
5344 }) else {
5345 continue;
5346 };
5347 let id = ObjectId(scene_objects.len());
5348 let kind = match cap.sub_type {
5349 CapSubType::Start => crate::frontend::api::CapKind::Start,
5350 CapSubType::End => crate::frontend::api::CapKind::End,
5351 };
5352 scene_objects.push(crate::front::Object {
5353 id,
5354 kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
5355 label: Default::default(),
5356 comments: Default::default(),
5357 artifact_id: cap.id,
5358 source: SourceRef::BackTrace {
5359 ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
5360 },
5361 });
5362 existing_artifact_ids.insert(cap.id);
5363 }
5364 _ => {}
5365 }
5366 }
5367}
5368
5369fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5370 use crate::engine::PlaneName;
5371
5372 match name {
5373 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5374 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5375 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5376 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5377 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5378 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5379 }
5380}
5381
5382fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5383 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5384 ast::UnaryOperator::Neg,
5385 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5386 )))
5387}
5388
5389fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5390 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5391 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5392 unlabeled: Some(solid_expr),
5393 arguments: vec![ast::LabeledArg {
5394 label: Some(ast::Identifier::new("face")),
5395 arg: face_expr,
5396 }],
5397 digest: None,
5398 non_code_meta: Default::default(),
5399 })))
5400}
5401
5402fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5403 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5404 return None;
5405 };
5406 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5407 return None;
5408 };
5409 if !matches!(
5410 sweep_call.callee.name.name.as_str(),
5411 "extrude" | "revolve" | "sweep" | "loft"
5412 ) {
5413 return None;
5414 }
5415 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5416 return None;
5417 };
5418 let candidate = region_name_expr.name.name.clone();
5419 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5420 return None;
5421 };
5422 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5423 return None;
5424 };
5425 if region_call.callee.name.name != "region" {
5426 return None;
5427 }
5428 Some(candidate)
5429}
5430
5431fn get_or_insert_ast_reference(
5438 ast: &mut ast::Node<ast::Program>,
5439 source_ref: &SourceRef,
5440 prefix: &str,
5441 property: Option<&str>,
5442) -> Result<ast::Expr, KclError> {
5443 let command = AstMutateCommand::AddVariableDeclaration {
5444 prefix: prefix.to_owned(),
5445 };
5446 let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
5447 let AstMutateCommandReturn::Name(var_name) = ret else {
5448 return Err(KclError::refactor(
5449 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5450 ));
5451 };
5452 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5453 let Some(property) = property else {
5454 return Ok(var_expr);
5456 };
5457
5458 Ok(create_member_expression(var_expr, property))
5459}
5460
5461fn mutate_ast_node_by_source_ref(
5462 ast: &mut ast::Node<ast::Program>,
5463 source_ref: &SourceRef,
5464 command: AstMutateCommand,
5465) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5466 let (source_range, node_path) = match source_ref {
5467 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5468 SourceRef::BackTrace { ranges } => {
5469 let [range] = ranges.as_slice() else {
5470 return Err(KclError::refactor(format!(
5471 "Expected single source ref, got {}; ranges={ranges:#?}",
5472 ranges.len(),
5473 )));
5474 };
5475 (range.0, range.1.clone())
5476 }
5477 };
5478 let mut context = AstMutateContext {
5479 source_range,
5480 node_path,
5481 command,
5482 defined_names_stack: Default::default(),
5483 };
5484 let control = dfs_mut(ast, &mut context);
5485 match control {
5486 ControlFlow::Continue(_) => Err(KclError::refactor(
5487 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5488 )),
5489 ControlFlow::Break(break_value) => break_value,
5490 }
5491}
5492
5493#[derive(Debug)]
5494struct AstMutateContext {
5495 source_range: SourceRange,
5496 node_path: Option<ast::NodePath>,
5497 command: AstMutateCommand,
5498 defined_names_stack: Vec<HashSet<String>>,
5499}
5500
5501#[derive(Debug)]
5502#[allow(clippy::large_enum_variant)]
5503enum AstMutateCommand {
5504 AddSketchBlockExprStmt {
5506 expr: ast::Expr,
5507 },
5508 AddSketchBlockVarDecl {
5510 prefix: String,
5511 expr: ast::Expr,
5512 },
5513 AddVariableDeclaration {
5514 prefix: String,
5515 },
5516 EditPoint {
5517 at: ast::Expr,
5518 },
5519 EditLine {
5520 start: ast::Expr,
5521 end: ast::Expr,
5522 construction: Option<bool>,
5523 },
5524 EditArc {
5525 start: ast::Expr,
5526 end: ast::Expr,
5527 center: ast::Expr,
5528 construction: Option<bool>,
5529 },
5530 EditCircle {
5531 start: ast::Expr,
5532 center: ast::Expr,
5533 construction: Option<bool>,
5534 },
5535 EditControlPointSpline {
5536 points: ast::Expr,
5537 construction: Option<bool>,
5538 },
5539 EditConstraintValue {
5540 value: ast::BinaryPart,
5541 },
5542 EditDistanceConstraintLabelPosition {
5543 label_position: ast::Expr,
5544 },
5545 EditCallUnlabeled {
5546 arg: ast::Expr,
5547 },
5548 EditVarInitialValue {
5549 value: Number,
5550 },
5551 DeleteNode,
5552}
5553
5554impl AstMutateCommand {
5555 fn needs_defined_names_stack(&self) -> bool {
5556 matches!(
5557 self,
5558 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5559 )
5560 }
5561}
5562
5563#[derive(Debug)]
5564enum AstMutateCommandReturn {
5565 None,
5566 Name(String),
5567}
5568
5569#[derive(Debug, Clone)]
5570struct AstNodeRef {
5571 range: SourceRange,
5572 node_path: Option<ast::NodePath>,
5573}
5574
5575impl<T> From<&ast::Node<T>> for AstNodeRef {
5576 fn from(value: &ast::Node<T>) -> Self {
5577 AstNodeRef {
5578 range: value.into(),
5579 node_path: value.node_path.clone(),
5580 }
5581 }
5582}
5583
5584impl From<&ast::BodyItem> for AstNodeRef {
5585 fn from(value: &ast::BodyItem) -> Self {
5586 match value {
5587 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5588 range: node.into(),
5589 node_path: node.node_path.clone(),
5590 },
5591 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5592 range: node.into(),
5593 node_path: node.node_path.clone(),
5594 },
5595 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5596 range: node.into(),
5597 node_path: node.node_path.clone(),
5598 },
5599 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5600 range: node.into(),
5601 node_path: node.node_path.clone(),
5602 },
5603 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5604 range: node.into(),
5605 node_path: node.node_path.clone(),
5606 },
5607 }
5608 }
5609}
5610
5611impl From<&ast::Expr> for AstNodeRef {
5612 fn from(value: &ast::Expr) -> Self {
5613 AstNodeRef {
5614 range: SourceRange::from(value),
5615 node_path: value.node_path().cloned(),
5616 }
5617 }
5618}
5619
5620impl From<&AstMutateContext> for AstNodeRef {
5621 fn from(value: &AstMutateContext) -> Self {
5622 AstNodeRef {
5623 range: value.source_range,
5624 node_path: value.node_path.clone(),
5625 }
5626 }
5627}
5628
5629impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5630 type Error = crate::walk::AstNodeError;
5631
5632 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5633 Ok(AstNodeRef {
5634 range: SourceRange::try_from(value)?,
5635 node_path: value.try_into()?,
5636 })
5637 }
5638}
5639
5640impl From<AstNodeRef> for SourceRange {
5641 fn from(value: AstNodeRef) -> Self {
5642 value.range
5643 }
5644}
5645
5646impl Visitor for AstMutateContext {
5647 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5648 type Continue = ();
5649
5650 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5651 filter_and_process(self, node)
5652 }
5653
5654 fn finish(&mut self, node: NodeMut<'_>) {
5655 match &node {
5656 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5657 self.defined_names_stack.pop();
5658 }
5659 _ => {}
5660 }
5661 }
5662}
5663
5664fn filter_and_process(
5665 ctx: &mut AstMutateContext,
5666 node: NodeMut,
5667) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5668 let Ok(node_range) = SourceRange::try_from(&node) else {
5669 return TraversalReturn::new_continue(());
5671 };
5672 if let NodeMut::VariableDeclaration(var_decl) = &node {
5677 let expr_range = SourceRange::from(&var_decl.declaration.init);
5678 let expr_node_path = var_decl.declaration.init.node_path();
5679 if source_ref_matches(ctx, expr_range, expr_node_path) {
5680 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5681 return TraversalReturn::new_break(Ok((
5684 AstNodeRef::from(&**var_decl),
5685 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5686 )));
5687 }
5688 if let AstMutateCommand::DeleteNode = &ctx.command {
5689 return TraversalReturn {
5692 mutate_body_item: MutateBodyItem::Delete,
5693 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5694 };
5695 }
5696 }
5697 }
5698 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5701 let expr_range = SourceRange::from(&expr_stmt.expression);
5702 let expr_node_path = expr_stmt.expression.node_path();
5703 if source_ref_matches(ctx, expr_range, expr_node_path) {
5704 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5705 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5708 return TraversalReturn::new_continue(());
5709 };
5710 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5711 }
5712 if let AstMutateCommand::DeleteNode = &ctx.command {
5713 return TraversalReturn {
5716 mutate_body_item: MutateBodyItem::Delete,
5717 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5718 };
5719 }
5720 }
5721 }
5722
5723 if ctx.command.needs_defined_names_stack() {
5724 if let NodeMut::Program(program) = &node {
5725 ctx.defined_names_stack.push(find_defined_names(*program));
5726 } else if let NodeMut::SketchBlock(block) = &node {
5727 ctx.defined_names_stack.push(find_defined_names(&block.body));
5728 }
5729 }
5730
5731 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5733 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5734 return TraversalReturn::new_continue(());
5735 }
5736 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5737 return TraversalReturn::new_continue(());
5738 };
5739 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5740}
5741
5742fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5743 match &ctx.node_path {
5744 Some(target) => Some(target) == node_path,
5745 None => node_range == ctx.source_range,
5746 }
5747}
5748
5749fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5750 match &ctx.command {
5751 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5752 if let NodeMut::SketchBlock(sketch_block) = node {
5753 sketch_block
5754 .body
5755 .items
5756 .push(ast::BodyItem::ExpressionStatement(ast::Node {
5757 inner: ast::ExpressionStatement {
5758 expression: expr.clone(),
5759 digest: None,
5760 },
5761 start: Default::default(),
5762 end: Default::default(),
5763 module_id: Default::default(),
5764 node_path: None,
5765 outer_attrs: Default::default(),
5766 pre_comments: Default::default(),
5767 comment_start: Default::default(),
5768 }));
5769 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5770 }
5771 }
5772 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5773 if let NodeMut::SketchBlock(sketch_block) = node {
5774 let empty_defined_names = HashSet::new();
5775 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5776 let Ok(name) = next_free_name(prefix, defined_names) else {
5777 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5778 };
5779 sketch_block
5780 .body
5781 .items
5782 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5783 ast::VariableDeclaration::new(
5784 ast::VariableDeclarator::new(&name, expr.clone()),
5785 ast::ItemVisibility::Default,
5786 ast::VariableKind::Const,
5787 ),
5788 ))));
5789 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5790 }
5791 }
5792 AstMutateCommand::AddVariableDeclaration { prefix } => {
5793 if let NodeMut::VariableDeclaration(inner) = node {
5794 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5795 }
5796 if let NodeMut::ExpressionStatement(expr_stmt) = node {
5797 let empty_defined_names = HashSet::new();
5798 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5799 let Ok(name) = next_free_name(prefix, defined_names) else {
5800 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5802 };
5803 let mutate_node =
5804 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5805 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5806 ast::ItemVisibility::Default,
5807 ast::VariableKind::Const,
5808 ))));
5809 return TraversalReturn {
5810 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5811 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5812 };
5813 }
5814 }
5815 AstMutateCommand::EditPoint { at } => {
5816 if let NodeMut::CallExpressionKw(call) = node {
5817 if call.callee.name.name != POINT_FN {
5818 return TraversalReturn::new_continue(());
5819 }
5820 for labeled_arg in &mut call.arguments {
5822 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5823 labeled_arg.arg = at.clone();
5824 }
5825 }
5826 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5827 }
5828 }
5829 AstMutateCommand::EditLine {
5830 start,
5831 end,
5832 construction,
5833 } => {
5834 if let NodeMut::CallExpressionKw(call) = node {
5835 if call.callee.name.name != LINE_FN {
5836 return TraversalReturn::new_continue(());
5837 }
5838 for labeled_arg in &mut call.arguments {
5840 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5841 labeled_arg.arg = start.clone();
5842 }
5843 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5844 labeled_arg.arg = end.clone();
5845 }
5846 }
5847 if let Some(construction_value) = construction {
5849 let construction_exists = call
5850 .arguments
5851 .iter()
5852 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5853 if *construction_value {
5854 if construction_exists {
5856 for labeled_arg in &mut call.arguments {
5858 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5859 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5860 value: ast::LiteralValue::Bool(true),
5861 raw: "true".to_string(),
5862 digest: None,
5863 })));
5864 }
5865 }
5866 } else {
5867 call.arguments.push(ast::LabeledArg {
5869 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5870 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5871 value: ast::LiteralValue::Bool(true),
5872 raw: "true".to_string(),
5873 digest: None,
5874 }))),
5875 });
5876 }
5877 } else {
5878 call.arguments
5880 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5881 }
5882 }
5883 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5884 }
5885 }
5886 AstMutateCommand::EditArc {
5887 start,
5888 end,
5889 center,
5890 construction,
5891 } => {
5892 if let NodeMut::CallExpressionKw(call) = node {
5893 if call.callee.name.name != ARC_FN {
5894 return TraversalReturn::new_continue(());
5895 }
5896 for labeled_arg in &mut call.arguments {
5898 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5899 labeled_arg.arg = start.clone();
5900 }
5901 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5902 labeled_arg.arg = end.clone();
5903 }
5904 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5905 labeled_arg.arg = center.clone();
5906 }
5907 }
5908 if let Some(construction_value) = construction {
5910 let construction_exists = call
5911 .arguments
5912 .iter()
5913 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5914 if *construction_value {
5915 if construction_exists {
5917 for labeled_arg in &mut call.arguments {
5919 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5920 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5921 value: ast::LiteralValue::Bool(true),
5922 raw: "true".to_string(),
5923 digest: None,
5924 })));
5925 }
5926 }
5927 } else {
5928 call.arguments.push(ast::LabeledArg {
5930 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5931 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5932 value: ast::LiteralValue::Bool(true),
5933 raw: "true".to_string(),
5934 digest: None,
5935 }))),
5936 });
5937 }
5938 } else {
5939 call.arguments
5941 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5942 }
5943 }
5944 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5945 }
5946 }
5947 AstMutateCommand::EditCircle {
5948 start,
5949 center,
5950 construction,
5951 } => {
5952 if let NodeMut::CallExpressionKw(call) = node {
5953 if call.callee.name.name != CIRCLE_FN {
5954 return TraversalReturn::new_continue(());
5955 }
5956 for labeled_arg in &mut call.arguments {
5958 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5959 labeled_arg.arg = start.clone();
5960 }
5961 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5962 labeled_arg.arg = center.clone();
5963 }
5964 }
5965 if let Some(construction_value) = construction {
5967 let construction_exists = call
5968 .arguments
5969 .iter()
5970 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5971 if *construction_value {
5972 if construction_exists {
5973 for labeled_arg in &mut call.arguments {
5975 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5976 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5977 value: ast::LiteralValue::Bool(true),
5978 raw: "true".to_string(),
5979 digest: None,
5980 })));
5981 }
5982 }
5983 } else {
5984 call.arguments.push(ast::LabeledArg {
5986 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5987 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5988 value: ast::LiteralValue::Bool(true),
5989 raw: "true".to_string(),
5990 digest: None,
5991 }))),
5992 });
5993 }
5994 } else {
5995 call.arguments
5997 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5998 }
5999 }
6000 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6001 }
6002 }
6003 AstMutateCommand::EditControlPointSpline { points, construction } => {
6004 if let NodeMut::CallExpressionKw(call) = node {
6005 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6006 return TraversalReturn::new_continue(());
6007 }
6008 for labeled_arg in &mut call.arguments {
6009 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6010 {
6011 labeled_arg.arg = points.clone();
6012 }
6013 }
6014 if let Some(construction_value) = construction {
6016 let construction_exists = call
6017 .arguments
6018 .iter()
6019 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6020 if *construction_value {
6021 if construction_exists {
6022 for labeled_arg in &mut call.arguments {
6023 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6024 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6025 value: ast::LiteralValue::Bool(true),
6026 raw: "true".to_string(),
6027 digest: None,
6028 })));
6029 }
6030 }
6031 } else {
6032 call.arguments.push(ast::LabeledArg {
6033 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6034 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6035 value: ast::LiteralValue::Bool(true),
6036 raw: "true".to_string(),
6037 digest: None,
6038 }))),
6039 });
6040 }
6041 } else {
6042 call.arguments
6043 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6044 }
6045 }
6046 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6047 }
6048 }
6049 AstMutateCommand::EditConstraintValue { value } => {
6050 if let NodeMut::BinaryExpression(binary_expr) = node {
6051 let left_is_constraint = matches!(
6052 &binary_expr.left,
6053 ast::BinaryPart::CallExpressionKw(call)
6054 if matches!(
6055 call.callee.name.name.as_str(),
6056 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
6057 )
6058 );
6059 if left_is_constraint {
6060 binary_expr.right = value.clone();
6061 } else {
6062 binary_expr.left = value.clone();
6063 }
6064
6065 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6066 }
6067 }
6068 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6069 if let NodeMut::BinaryExpression(binary_expr) = node {
6070 let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
6071 return TraversalReturn::new_continue(());
6072 };
6073 if !matches!(
6074 call.callee.name.name.as_str(),
6075 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
6076 ) {
6077 return TraversalReturn::new_continue(());
6078 }
6079
6080 if let Some(label_arg) = call
6081 .arguments
6082 .iter_mut()
6083 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6084 {
6085 label_arg.arg = label_position.clone();
6086 } else {
6087 call.arguments.push(ast::LabeledArg {
6088 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6089 arg: label_position.clone(),
6090 });
6091 }
6092
6093 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6094 }
6095 }
6096 AstMutateCommand::EditCallUnlabeled { arg } => {
6097 if let NodeMut::CallExpressionKw(call) = node {
6098 call.unlabeled = Some(arg.clone());
6099 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6100 }
6101 }
6102 AstMutateCommand::EditVarInitialValue { value } => {
6103 if let NodeMut::SketchVar(sketch_var) = node {
6107 let Ok(literal) = to_source_number(*value) else {
6108 return TraversalReturn::new_break(Err(KclError::refactor(format!(
6109 "Could not convert number to AST literal: {:?}",
6110 *value
6111 ))));
6112 };
6113 sketch_var.initial = Some(Box::new(ast::Node::no_src(literal)));
6114 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6115 }
6116 }
6117 AstMutateCommand::DeleteNode => {
6118 return TraversalReturn {
6119 mutate_body_item: MutateBodyItem::Delete,
6120 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6121 };
6122 }
6123 }
6124 TraversalReturn::new_continue(())
6125}
6126
6127struct FindSketchBlockSourceRange {
6128 target_before_mutation: SourceRange,
6130 found: Cell<Option<AstNodeRef>>,
6134}
6135
6136impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6137 type Error = crate::front::Error;
6138
6139 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6140 let Ok(node_range) = SourceRange::try_from(&node) else {
6141 return Ok(true);
6142 };
6143
6144 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6145 if node_range.module_id() == self.target_before_mutation.module_id()
6146 && node_range.start() == self.target_before_mutation.start()
6147 && node_range.end() >= self.target_before_mutation.end()
6149 {
6150 self.found.set(sketch_block.body.items.last().map(|item| match item {
6151 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6155 _ => AstNodeRef::from(item),
6156 }));
6157 return Ok(false);
6158 } else {
6159 return Ok(true);
6162 }
6163 }
6164
6165 for child in node.children().iter() {
6166 if !child.visit(*self)? {
6167 return Ok(false);
6168 }
6169 }
6170
6171 Ok(true)
6172 }
6173}
6174
6175struct FindSketchBlockByNodePath {
6176 target_node_path: ast::NodePath,
6178 found: Cell<Option<AstNodeRef>>,
6182}
6183
6184impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6185 type Error = crate::front::Error;
6186
6187 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6188 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6189 return Ok(true);
6190 };
6191
6192 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6193 if let Some(node_path) = node_path
6194 && node_path == self.target_node_path
6195 {
6196 self.found.set(sketch_block.body.items.last().map(|item| match item {
6197 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6201 _ => AstNodeRef::from(item),
6202 }));
6203
6204 return Ok(false);
6205 } else {
6206 return Ok(true);
6209 }
6210 }
6211
6212 for child in node.children().iter() {
6213 if !child.visit(*self)? {
6214 return Ok(false);
6215 }
6216 }
6217
6218 Ok(true)
6219 }
6220}
6221
6222fn find_sketch_block_added_item(
6230 ast: &ast::Node<ast::Program>,
6231 sketch_block_before_mutation: &AstNodeRef,
6232) -> Result<AstNodeRef, KclError> {
6233 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6234 let find = FindSketchBlockByNodePath {
6235 target_node_path: node_path.clone(),
6236 found: Cell::new(None),
6237 };
6238 let node = crate::walk::Node::from(ast);
6239 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6240 find.found.into_inner().ok_or_else(|| {
6241 KclError::refactor(format!(
6242 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6243 ))
6244 })
6245 } else {
6246 let find = FindSketchBlockSourceRange {
6248 target_before_mutation: sketch_block_before_mutation.range,
6249 found: Cell::new(None),
6250 };
6251 let node = crate::walk::Node::from(ast);
6252 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6253 find.found.into_inner().ok_or_else(|| KclError::refactor(
6254 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?"),
6255 ))
6256 }
6257}
6258
6259fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6260 ast.recast_top(&Default::default(), 0)
6262}
6263
6264struct FindNumericLiteral {
6265 target: SourceRange,
6266 found: Cell<Option<ast::NumericLiteral>>,
6267}
6268
6269impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6270 type Error = crate::front::Error;
6271
6272 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6273 let Ok(node_range) = SourceRange::try_from(&node) else {
6274 return Ok(true);
6275 };
6276
6277 if node_range == self.target
6278 && let crate::walk::Node::NumericLiteral(literal) = node
6279 {
6280 self.found.set(Some(literal.inner.clone()));
6281 return Ok(false);
6282 }
6283
6284 for child in node.children().iter() {
6285 if !child.visit(*self)? {
6286 return Ok(false);
6287 }
6288 }
6289
6290 Ok(true)
6291 }
6292}
6293
6294fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6295 let find = FindNumericLiteral {
6296 target,
6297 found: Cell::new(None),
6298 };
6299 let node = crate::walk::Node::from(ast);
6300 node.visit(&find).ok()?;
6301 find.found.into_inner()
6302}
6303
6304struct FindSketchVarInitialByNodePath<'a> {
6305 target: &'a ast::NodePath,
6306 sketch_var_found: Cell<bool>,
6307 initial_literal: Cell<Option<ast::NumericLiteral>>,
6308}
6309
6310impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6311 type Error = crate::front::Error;
6312
6313 fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6314 if let crate::walk::Node::SketchVar(sketch_var) = node
6315 && sketch_var.node_path.as_ref() == Some(self.target)
6316 {
6317 self.sketch_var_found.set(true);
6318 if let Some(initial) = &sketch_var.initial {
6319 self.initial_literal.set(Some(initial.inner.clone()));
6320 }
6321 return Ok(false);
6322 }
6323
6324 for child in node.children().iter() {
6325 if !child.visit(*self)? {
6326 return Ok(false);
6327 }
6328 }
6329
6330 Ok(true)
6331 }
6332}
6333
6334fn numeric_literal_at_node_path(
6344 ast: &ast::Node<ast::Program>,
6345 node_path: Option<&ast::NodePath>,
6346 source_range: SourceRange,
6347) -> Option<Option<ast::NumericLiteral>> {
6348 let Some(node_path) = node_path else {
6349 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";
6350 #[cfg(target_arch = "wasm32")]
6351 web_sys::console::warn_1(&message.into());
6352 #[cfg(not(target_arch = "wasm32"))]
6353 eprintln!("WARNING: {message}");
6354 return numeric_literal_at_source_range(ast, source_range).map(Some);
6355 };
6356 let find = FindSketchVarInitialByNodePath {
6357 target: node_path,
6358 sketch_var_found: Cell::new(false),
6359 initial_literal: Cell::new(None),
6360 };
6361 let node = crate::walk::Node::from(ast);
6362 node.visit(&find).ok()?;
6363 if !find.sketch_var_found.get() {
6364 return None;
6365 }
6366 Some(find.initial_literal.into_inner())
6367}
6368
6369fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6370 match suffix {
6371 NumericSuffix::Mm => Some(UnitLength::Millimeters),
6372 NumericSuffix::Cm => Some(UnitLength::Centimeters),
6373 NumericSuffix::M => Some(UnitLength::Meters),
6374 NumericSuffix::Inch => Some(UnitLength::Inches),
6375 NumericSuffix::Ft => Some(UnitLength::Feet),
6376 NumericSuffix::Yd => Some(UnitLength::Yards),
6377 _ => None,
6378 }
6379}
6380
6381fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6382 match suffix_length_unit(number.units) {
6383 Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6384 None => number.value,
6385 }
6386}
6387
6388fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6389 match suffix_length_unit(literal.suffix) {
6390 Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6391 None => literal.value,
6392 }
6393}
6394
6395fn var_solution_needs_commit(
6396 current_literal: &ast::NumericLiteral,
6397 solved_value: Number,
6398 default_length_unit: UnitLength,
6399) -> bool {
6400 let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6401 let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6402
6403 (current - solved).abs() > 1e-9
6404}
6405
6406fn preserve_var_solution_literal_style(
6407 current_literal: &ast::NumericLiteral,
6408 solved_value: Number,
6409 default_length_unit: UnitLength,
6410) -> Number {
6411 if current_literal.suffix == NumericSuffix::None {
6412 return Number {
6413 value: number_value_in_default_length_units(solved_value, default_length_unit),
6414 units: NumericSuffix::None,
6415 };
6416 }
6417
6418 let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6419 return solved_value;
6420 };
6421
6422 let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6423 Number {
6424 value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6425 units: current_literal.suffix,
6426 }
6427}
6428
6429pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6430 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
6431 inner: ast::ArrayExpression {
6432 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6433 non_code_meta: Default::default(),
6434 digest: None,
6435 },
6436 start: Default::default(),
6437 end: Default::default(),
6438 module_id: Default::default(),
6439 node_path: None,
6440 outer_attrs: Default::default(),
6441 pre_comments: Default::default(),
6442 comment_start: Default::default(),
6443 })))
6444}
6445
6446pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6447 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6448 ast::ArrayExpression {
6449 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6450 digest: None,
6451 non_code_meta: Default::default(),
6452 },
6453 ))))
6454}
6455
6456fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6457 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6458 ast::ArrayExpression {
6459 elements: vec![
6460 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6461 point.x,
6462 )?)))),
6463 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6464 point.y,
6465 )?)))),
6466 ],
6467 non_code_meta: Default::default(),
6468 digest: None,
6469 },
6470 ))))
6471}
6472
6473fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6474 match expr {
6475 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
6476 inner: ast::Literal::from(to_source_number(*number)?),
6477 start: Default::default(),
6478 end: Default::default(),
6479 module_id: Default::default(),
6480 node_path: None,
6481 outer_attrs: Default::default(),
6482 pre_comments: Default::default(),
6483 comment_start: Default::default(),
6484 }))),
6485 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
6486 inner: ast::SketchVar {
6487 initial: Some(Box::new(ast::Node {
6488 inner: to_source_number(*number)?,
6489 start: Default::default(),
6490 end: Default::default(),
6491 module_id: Default::default(),
6492 node_path: None,
6493 outer_attrs: Default::default(),
6494 pre_comments: Default::default(),
6495 comment_start: Default::default(),
6496 })),
6497 digest: None,
6498 },
6499 start: Default::default(),
6500 end: Default::default(),
6501 module_id: Default::default(),
6502 node_path: None,
6503 outer_attrs: Default::default(),
6504 pre_comments: Default::default(),
6505 comment_start: Default::default(),
6506 }))),
6507 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6508 }
6509}
6510
6511fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6512 Ok(ast::NumericLiteral {
6513 value: number.value,
6514 suffix: number.units,
6515 raw: format_number_literal(number.value, number.units, None)?,
6516 digest: None,
6517 })
6518}
6519
6520pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
6521 ast::Expr::Name(Box::new(ast_name(name)))
6522}
6523
6524fn ast_name(name: String) -> ast::Node<ast::Name> {
6525 ast::Node {
6526 inner: ast::Name {
6527 name: ast::Node {
6528 inner: ast::Identifier { name, digest: None },
6529 start: Default::default(),
6530 end: Default::default(),
6531 module_id: Default::default(),
6532 node_path: None,
6533 outer_attrs: Default::default(),
6534 pre_comments: Default::default(),
6535 comment_start: Default::default(),
6536 },
6537 path: Vec::new(),
6538 abs_path: false,
6539 digest: None,
6540 },
6541 start: Default::default(),
6542 end: Default::default(),
6543 module_id: Default::default(),
6544 node_path: None,
6545 outer_attrs: Default::default(),
6546 pre_comments: Default::default(),
6547 comment_start: Default::default(),
6548 }
6549}
6550
6551pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
6552 ast::Name {
6553 name: ast::Node {
6554 inner: ast::Identifier {
6555 name: name.to_owned(),
6556 digest: None,
6557 },
6558 start: Default::default(),
6559 end: Default::default(),
6560 module_id: Default::default(),
6561 node_path: None,
6562 outer_attrs: Default::default(),
6563 pre_comments: Default::default(),
6564 comment_start: Default::default(),
6565 },
6566 path: Default::default(),
6567 abs_path: false,
6568 digest: None,
6569 }
6570}
6571
6572pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6576 let elements = exprs.into_iter().collect::<Vec<_>>();
6577 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6578
6579 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6581 elements,
6582 digest: None,
6583 non_code_meta: Default::default(),
6584 })));
6585
6586 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6588 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6589 unlabeled: Some(array_expr),
6590 arguments: Default::default(),
6591 digest: None,
6592 non_code_meta: Default::default(),
6593 })))
6594}
6595
6596pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6598 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6599 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6600 unlabeled: None,
6601 arguments: vec![
6602 ast::LabeledArg {
6603 label: Some(ast::Identifier::new(LINE_START_PARAM)),
6604 arg: start_ast,
6605 },
6606 ast::LabeledArg {
6607 label: Some(ast::Identifier::new(LINE_END_PARAM)),
6608 arg: end_ast,
6609 },
6610 ],
6611 digest: None,
6612 non_code_meta: Default::default(),
6613 })))
6614}
6615
6616pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6618 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6619 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6620 unlabeled: None,
6621 arguments: vec![
6622 ast::LabeledArg {
6623 label: Some(ast::Identifier::new(ARC_START_PARAM)),
6624 arg: start_ast,
6625 },
6626 ast::LabeledArg {
6627 label: Some(ast::Identifier::new(ARC_END_PARAM)),
6628 arg: end_ast,
6629 },
6630 ast::LabeledArg {
6631 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6632 arg: center_ast,
6633 },
6634 ],
6635 digest: None,
6636 non_code_meta: Default::default(),
6637 })))
6638}
6639
6640pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6642 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6643 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6644 unlabeled: None,
6645 arguments: vec![
6646 ast::LabeledArg {
6647 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6648 arg: start_ast,
6649 },
6650 ast::LabeledArg {
6651 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6652 arg: center_ast,
6653 },
6654 ],
6655 digest: None,
6656 non_code_meta: Default::default(),
6657 })))
6658}
6659
6660pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6662 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6663 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6664 unlabeled: Some(line_expr),
6665 arguments: Default::default(),
6666 digest: None,
6667 non_code_meta: Default::default(),
6668 })))
6669}
6670
6671pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6673 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6674 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6675 unlabeled: Some(line_expr),
6676 arguments: Default::default(),
6677 digest: None,
6678 non_code_meta: Default::default(),
6679 })))
6680}
6681
6682pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6684 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6685 object: object_expr,
6686 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6687 name: ast::Node::no_src(ast::Identifier {
6688 name: property.to_string(),
6689 digest: None,
6690 }),
6691 path: Vec::new(),
6692 abs_path: false,
6693 digest: None,
6694 }))),
6695 computed: false,
6696 digest: None,
6697 })))
6698}
6699
6700pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
6701 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6702 object: object_expr,
6703 property: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
6704 value: index as f64,
6705 suffix: NumericSuffix::None,
6706 raw: index.to_string(),
6707 digest: None,
6708 })))),
6709 computed: true,
6710 digest: None,
6711 })))
6712}
6713
6714fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
6716 let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6718 position.x,
6719 )?))));
6720 let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6721 position.y,
6722 )?))));
6723 let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6724 elements: vec![x_literal, y_literal],
6725 digest: None,
6726 non_code_meta: Default::default(),
6727 })));
6728
6729 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6731 elements: vec![point_expr, point_array],
6732 digest: None,
6733 non_code_meta: Default::default(),
6734 })));
6735
6736 Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
6738 ast::CallExpressionKw {
6739 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
6740 unlabeled: Some(array_expr),
6741 arguments: Default::default(),
6742 digest: None,
6743 non_code_meta: Default::default(),
6744 },
6745 ))))
6746}
6747
6748pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
6750 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6751 elements: line_exprs,
6752 digest: None,
6753 non_code_meta: Default::default(),
6754 })));
6755
6756 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6758 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
6759 unlabeled: Some(array_expr),
6760 arguments: Default::default(),
6761 digest: None,
6762 non_code_meta: Default::default(),
6763 })))
6764}
6765
6766pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
6768 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6769 elements: segment_exprs,
6770 digest: None,
6771 non_code_meta: Default::default(),
6772 })));
6773
6774 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6775 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
6776 unlabeled: Some(array_expr),
6777 arguments: Default::default(),
6778 digest: None,
6779 non_code_meta: Default::default(),
6780 })))
6781}
6782
6783pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
6785 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6786 elements: vec![seg1_expr, seg2_expr],
6787 digest: None,
6788 non_code_meta: Default::default(),
6789 })));
6790
6791 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6792 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
6793 unlabeled: Some(array_expr),
6794 arguments: Default::default(),
6795 digest: None,
6796 non_code_meta: Default::default(),
6797 })))
6798}
6799
6800pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
6802 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6803 elements: input_exprs,
6804 digest: None,
6805 non_code_meta: Default::default(),
6806 })));
6807 let arguments = vec![ast::LabeledArg {
6808 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
6809 arg: axis_expr,
6810 }];
6811
6812 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6813 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
6814 unlabeled: Some(array_expr),
6815 arguments,
6816 digest: None,
6817 non_code_meta: Default::default(),
6818 })))
6819}
6820
6821pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
6823 let arguments = vec![ast::LabeledArg {
6824 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
6825 arg: point_expr,
6826 }];
6827
6828 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6829 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
6830 unlabeled: Some(segment_expr),
6831 arguments,
6832 digest: None,
6833 non_code_meta: Default::default(),
6834 })))
6835}
6836
6837#[cfg(test)]
6838mod tests {
6839 use super::*;
6840 use crate::engine::PlaneName;
6841 use crate::execution::cache::SketchModeState;
6842 use crate::execution::cache::clear_mem_cache;
6843 use crate::execution::cache::read_old_memory;
6844 use crate::execution::cache::write_old_memory;
6845 use crate::front::Distance;
6846 use crate::front::Fixed;
6847 use crate::front::FixedPoint;
6848 use crate::front::Midpoint;
6849 use crate::front::Object;
6850 use crate::front::Plane;
6851 use crate::front::Sketch;
6852 use crate::front::Tangent;
6853 use crate::frontend::sketch::Vertical;
6854 use crate::pretty::NumericSuffix;
6855
6856 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
6857 for object in &scene_graph.objects {
6858 if let ObjectKind::Sketch(_) = &object.kind {
6859 return Some(object);
6860 }
6861 }
6862 None
6863 }
6864
6865 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
6866 for object in &scene_graph.objects {
6867 if let ObjectKind::Face(_) = &object.kind {
6868 return Some(object);
6869 }
6870 }
6871 None
6872 }
6873
6874 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
6875 for object in &scene_graph.objects {
6876 if matches!(&object.kind, ObjectKind::Wall(_)) {
6877 return Some(object.id);
6878 }
6879 }
6880 None
6881 }
6882
6883 #[test]
6884 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
6885 let source = "\
6886region001 = region(point = [0.1, 0.1], sketch = s)
6887extrude001 = extrude(region001, length = 5)
6888revolve001 = revolve(region001, axis = Y)
6889sweep001 = sweep(region001, path = path001)
6890loft001 = loft(region001)
6891not_sweep001 = shell(extrude001, faces = [], thickness = 1)
6892";
6893
6894 let program = Program::parse(source).unwrap().0.unwrap();
6895
6896 assert_eq!(
6897 region_name_from_sweep_variable(&program.ast, "extrude001"),
6898 Some("region001".to_owned())
6899 );
6900 assert_eq!(
6901 region_name_from_sweep_variable(&program.ast, "revolve001"),
6902 Some("region001".to_owned())
6903 );
6904 assert_eq!(
6905 region_name_from_sweep_variable(&program.ast, "sweep001"),
6906 Some("region001".to_owned())
6907 );
6908 assert_eq!(
6909 region_name_from_sweep_variable(&program.ast, "loft001"),
6910 Some("region001".to_owned())
6911 );
6912 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
6913 }
6914
6915 #[track_caller]
6916 fn expect_sketch(object: &Object) -> &Sketch {
6917 if let ObjectKind::Sketch(sketch) = &object.kind {
6918 sketch
6919 } else {
6920 panic!("Object is not a sketch: {:?}", object);
6921 }
6922 }
6923
6924 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
6925 let point_object = scene_graph.objects.get(point_id.0).unwrap();
6926 let ObjectKind::Segment {
6927 segment: Segment::Point(point),
6928 } = &point_object.kind
6929 else {
6930 panic!("Object is not a point segment: {point_object:?}");
6931 };
6932 point.position.clone()
6933 }
6934
6935 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
6936 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
6937 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
6938 }
6939
6940 fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
6943 Point2d {
6944 x: Expr::Var(Number {
6945 value: x,
6946 units: NumericSuffix::Mm,
6947 }),
6948 y: Expr::Var(Number {
6949 value: y,
6950 units: NumericSuffix::Mm,
6951 }),
6952 }
6953 }
6954
6955 fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
6958 Point2d {
6959 x: Number {
6960 value: x,
6961 units: NumericSuffix::Mm,
6962 },
6963 y: Number {
6964 value: y,
6965 units: NumericSuffix::Mm,
6966 },
6967 }
6968 }
6969
6970 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
6971 LineCtor {
6972 start: Point2d {
6973 x: Expr::Number(Number { value: start_x, units }),
6974 y: Expr::Number(Number { value: start_y, units }),
6975 },
6976 end: Point2d {
6977 x: Expr::Number(Number { value: end_x, units }),
6978 y: Expr::Number(Number { value: end_y, units }),
6979 },
6980 construction: None,
6981 }
6982 }
6983
6984 async fn create_sketch_with_single_line(
6985 frontend: &mut FrontendState,
6986 ctx: &ExecutorContext,
6987 mock_ctx: &ExecutorContext,
6988 version: Version,
6989 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
6990 frontend.program = Program::empty();
6991
6992 let sketch_args = SketchCtor {
6993 on: Plane::Default(PlaneName::Xy),
6994 };
6995 let (_src_delta, _scene_delta, sketch_id) = frontend
6996 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
6997 .await
6998 .unwrap();
6999
7000 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7001 let (source_delta, scene_graph_delta) = frontend
7002 .add_segment(mock_ctx, version, sketch_id, segment, None)
7003 .await
7004 .unwrap();
7005 let line_id = *scene_graph_delta
7006 .new_objects
7007 .last()
7008 .expect("Expected line object id to be created");
7009
7010 (sketch_id, line_id, source_delta, scene_graph_delta)
7011 }
7012
7013 async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7014 frontend.program = program.clone();
7015 let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7016 frontend.update_state_after_exec(outcome, true);
7017 }
7018
7019 #[tokio::test(flavor = "multi_thread")]
7020 async fn test_sketch_checkpoint_round_trip_restores_state() {
7021 let mut frontend = FrontendState::new();
7022 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7023 let mock_ctx = ExecutorContext::new_mock(None).await;
7024 let version = Version(0);
7025
7026 let (sketch_id, line_id, source_delta, scene_graph_delta) =
7027 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7028
7029 let expected_source = source_delta.text.clone();
7030 let expected_scene_graph = frontend.scene_graph.clone();
7031 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7032 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7033
7034 let checkpoint_id = frontend
7035 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7036 .await
7037 .unwrap();
7038
7039 let edited_segments = vec![ExistingSegmentCtor {
7040 id: line_id,
7041 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7042 }];
7043 let (edited_source, _edited_scene) = frontend
7044 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7045 .await
7046 .unwrap();
7047 assert_ne!(edited_source.text, expected_source);
7048
7049 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7050
7051 assert_eq!(restored.source_delta.text, expected_source);
7052 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7053 assert!(restored.scene_graph_delta.invalidates_ids);
7054 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7055 assert_eq!(frontend.scene_graph, expected_scene_graph);
7056 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7057
7058 ctx.close().await;
7059 }
7060
7061 #[tokio::test(flavor = "multi_thread")]
7062 async fn test_sketch_checkpoints_prune_oldest_entries() {
7063 let mut frontend = FrontendState::new();
7064 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7065 let mock_ctx = ExecutorContext::new_mock(None).await;
7066 let version = Version(0);
7067
7068 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7069 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7070
7071 let mut checkpoint_ids = Vec::new();
7072 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7073 checkpoint_ids.push(
7074 frontend
7075 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7076 .await
7077 .unwrap(),
7078 );
7079 }
7080
7081 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7082 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7083
7084 let oldest_retained = checkpoint_ids[3];
7085 assert_eq!(
7086 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7087 Some(oldest_retained)
7088 );
7089
7090 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7091 assert!(evicted_restore.is_err());
7092 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7093
7094 frontend
7095 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7096 .await
7097 .unwrap();
7098
7099 ctx.close().await;
7100 }
7101
7102 #[tokio::test(flavor = "multi_thread")]
7103 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7104 let mut frontend = FrontendState::new();
7105 let missing_checkpoint = SketchCheckpointId::new(999);
7106
7107 let err = frontend
7108 .restore_sketch_checkpoint(missing_checkpoint)
7109 .await
7110 .expect_err("Expected restore to fail for missing checkpoint");
7111
7112 assert!(err.msg.contains("Sketch checkpoint not found"));
7113 }
7114
7115 #[tokio::test(flavor = "multi_thread")]
7116 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7117 let mut frontend = FrontendState::new();
7118 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7119 let mock_ctx = ExecutorContext::new_mock(None).await;
7120 let version = Version(0);
7121
7122 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7123 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7124
7125 let checkpoint_a = frontend
7126 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7127 .await
7128 .unwrap();
7129 let checkpoint_b = frontend
7130 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7131 .await
7132 .unwrap();
7133 assert_eq!(frontend.sketch_checkpoints.len(), 2);
7134
7135 frontend.clear_sketch_checkpoints();
7136 assert!(frontend.sketch_checkpoints.is_empty());
7137 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7138 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7139
7140 ctx.close().await;
7141 }
7142
7143 #[tokio::test(flavor = "multi_thread")]
7144 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7145 let mut frontend = FrontendState::new();
7146 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7147 let mock_ctx = ExecutorContext::new_mock(None).await;
7148 let version = Version(0);
7149
7150 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7151 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7152 let old_source = source_delta.text.clone();
7153 let old_checkpoint = frontend
7154 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7155 .await
7156 .unwrap();
7157 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7158
7159 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
7160 .unwrap()
7161 .0
7162 .unwrap();
7163
7164 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7165 let SetProgramOutcome::Success {
7166 checkpoint_id: Some(new_checkpoint),
7167 ..
7168 } = result
7169 else {
7170 panic!("Expected Success with a fresh checkpoint baseline");
7171 };
7172
7173 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7174
7175 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7176 assert_eq!(old_restore.source_delta.text, old_source);
7177
7178 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7179 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7180
7181 ctx.close().await;
7182 }
7183
7184 #[tokio::test(flavor = "multi_thread")]
7185 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7186 let mut frontend = FrontendState::new();
7187 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7188 let mock_ctx = ExecutorContext::new_mock(None).await;
7189 let version = Version(0);
7190
7191 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7192 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7193 let old_checkpoint = frontend
7194 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7195 .await
7196 .unwrap();
7197 let checkpoint_count_before = frontend.sketch_checkpoints.len();
7198
7199 let failing_program = Program::parse(
7200 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7201 )
7202 .unwrap()
7203 .0
7204 .unwrap();
7205
7206 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7207 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7208 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7209 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7210
7211 ctx.close().await;
7212 }
7213
7214 #[tokio::test(flavor = "multi_thread")]
7215 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7216 let mut frontend = FrontendState::new();
7217 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7218
7219 let program = Program::parse(
7220 "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",
7221 )
7222 .unwrap()
7223 .0
7224 .unwrap();
7225 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7226 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7227 panic!("Expected successful baseline program execution");
7228 };
7229
7230 clear_mem_cache().await;
7231 assert!(read_old_memory().await.is_none());
7232
7233 let checkpoint_without_mock_memory = frontend
7234 .create_sketch_checkpoint((*exec_outcome).clone())
7235 .await
7236 .unwrap();
7237
7238 write_old_memory(SketchModeState::new_for_tests()).await;
7239 assert!(read_old_memory().await.is_some());
7240
7241 let checkpoint_with_mock_memory = frontend
7242 .create_sketch_checkpoint((*exec_outcome).clone())
7243 .await
7244 .unwrap();
7245
7246 clear_mem_cache().await;
7247 assert!(read_old_memory().await.is_none());
7248
7249 frontend
7250 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7251 .await
7252 .unwrap();
7253 assert!(read_old_memory().await.is_some());
7254
7255 frontend
7256 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7257 .await
7258 .unwrap();
7259 assert!(read_old_memory().await.is_none());
7260
7261 ctx.close().await;
7262 }
7263
7264 #[tokio::test(flavor = "multi_thread")]
7265 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7266 let source = "\
7267sketch(on = XY) {
7268 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7269}
7270
7271bad = missing_name
7272";
7273 let program = Program::parse(source).unwrap().0.unwrap();
7274
7275 let mut frontend = FrontendState::new();
7276
7277 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7278 let mock_ctx = ExecutorContext::new_mock(None).await;
7279 let version = Version(0);
7280 let project_id = ProjectId(0);
7281 let file_id = FileId(0);
7282
7283 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7284 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7285 };
7286
7287 let sketch_id = frontend
7288 .scene_graph
7289 .objects
7290 .iter()
7291 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7292 .expect("Expected sketch object from errored hack_set_program");
7293
7294 frontend
7295 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7296 .await
7297 .unwrap();
7298
7299 ctx.close().await;
7300 mock_ctx.close().await;
7301 }
7302
7303 #[tokio::test(flavor = "multi_thread")]
7304 async fn test_new_sketch_add_point_edit_point() {
7305 let program = Program::empty();
7306
7307 let mut frontend = FrontendState::new();
7308 frontend.program = program;
7309
7310 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7311 let mock_ctx = ExecutorContext::new_mock(None).await;
7312 let version = Version(0);
7313
7314 let sketch_args = SketchCtor {
7315 on: Plane::Default(PlaneName::Xy),
7316 };
7317 let (_src_delta, scene_delta, sketch_id) = frontend
7318 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7319 .await
7320 .unwrap();
7321 assert_eq!(sketch_id, ObjectId(1));
7322 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7323 let sketch_object = &scene_delta.new_graph.objects[1];
7324 assert_eq!(sketch_object.id, ObjectId(1));
7325 assert_eq!(
7326 sketch_object.kind,
7327 ObjectKind::Sketch(Sketch {
7328 args: SketchCtor {
7329 on: Plane::Default(PlaneName::Xy)
7330 },
7331 plane: ObjectId(0),
7332 segments: vec![],
7333 constraints: vec![],
7334 })
7335 );
7336 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7337
7338 let point_ctor = PointCtor {
7339 position: Point2d {
7340 x: Expr::Number(Number {
7341 value: 1.0,
7342 units: NumericSuffix::Inch,
7343 }),
7344 y: Expr::Number(Number {
7345 value: 2.0,
7346 units: NumericSuffix::Inch,
7347 }),
7348 },
7349 };
7350 let segment = SegmentCtor::Point(point_ctor);
7351 let (src_delta, scene_delta) = frontend
7352 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7353 .await
7354 .unwrap();
7355 assert_eq!(
7356 src_delta.text.as_str(),
7357 "sketch001 = sketch(on = XY) {
7358 point(at = [1in, 2in])
7359}
7360"
7361 );
7362 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7363 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7364 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7365 assert_eq!(scene_object.id.0, i);
7366 }
7367
7368 let point_id = *scene_delta.new_objects.last().unwrap();
7369
7370 let point_ctor = PointCtor {
7371 position: Point2d {
7372 x: Expr::Number(Number {
7373 value: 3.0,
7374 units: NumericSuffix::Inch,
7375 }),
7376 y: Expr::Number(Number {
7377 value: 4.0,
7378 units: NumericSuffix::Inch,
7379 }),
7380 },
7381 };
7382 let segments = vec![ExistingSegmentCtor {
7383 id: point_id,
7384 ctor: SegmentCtor::Point(point_ctor),
7385 }];
7386 let (src_delta, scene_delta) = frontend
7387 .edit_segments(&mock_ctx, version, sketch_id, segments)
7388 .await
7389 .unwrap();
7390 assert_eq!(
7391 src_delta.text.as_str(),
7392 "sketch001 = sketch(on = XY) {
7393 point(at = [3in, 4in])
7394}
7395"
7396 );
7397 assert_eq!(scene_delta.new_objects, vec![]);
7398 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7399
7400 ctx.close().await;
7401 mock_ctx.close().await;
7402 }
7403
7404 #[tokio::test(flavor = "multi_thread")]
7405 async fn test_new_sketch_add_line_edit_line() {
7406 let program = Program::empty();
7407
7408 let mut frontend = FrontendState::new();
7409 frontend.program = program;
7410
7411 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7412 let mock_ctx = ExecutorContext::new_mock(None).await;
7413 let version = Version(0);
7414
7415 let sketch_args = SketchCtor {
7416 on: Plane::Default(PlaneName::Xy),
7417 };
7418 let (_src_delta, scene_delta, sketch_id) = frontend
7419 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7420 .await
7421 .unwrap();
7422 assert_eq!(sketch_id, ObjectId(1));
7423 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7424 let sketch_object = &scene_delta.new_graph.objects[1];
7425 assert_eq!(sketch_object.id, ObjectId(1));
7426 assert_eq!(
7427 sketch_object.kind,
7428 ObjectKind::Sketch(Sketch {
7429 args: SketchCtor {
7430 on: Plane::Default(PlaneName::Xy)
7431 },
7432 plane: ObjectId(0),
7433 segments: vec![],
7434 constraints: vec![],
7435 })
7436 );
7437 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7438
7439 let line_ctor = LineCtor {
7440 start: Point2d {
7441 x: Expr::Number(Number {
7442 value: 0.0,
7443 units: NumericSuffix::Mm,
7444 }),
7445 y: Expr::Number(Number {
7446 value: 0.0,
7447 units: NumericSuffix::Mm,
7448 }),
7449 },
7450 end: Point2d {
7451 x: Expr::Number(Number {
7452 value: 10.0,
7453 units: NumericSuffix::Mm,
7454 }),
7455 y: Expr::Number(Number {
7456 value: 10.0,
7457 units: NumericSuffix::Mm,
7458 }),
7459 },
7460 construction: None,
7461 };
7462 let segment = SegmentCtor::Line(line_ctor);
7463 let (src_delta, scene_delta) = frontend
7464 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7465 .await
7466 .unwrap();
7467 assert_eq!(
7468 src_delta.text.as_str(),
7469 "sketch001 = sketch(on = XY) {
7470 line(start = [0mm, 0mm], end = [10mm, 10mm])
7471}
7472"
7473 );
7474 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7475 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7476 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7477 assert_eq!(scene_object.id.0, i);
7478 }
7479
7480 let line = *scene_delta.new_objects.last().unwrap();
7482
7483 let line_ctor = LineCtor {
7484 start: Point2d {
7485 x: Expr::Number(Number {
7486 value: 1.0,
7487 units: NumericSuffix::Mm,
7488 }),
7489 y: Expr::Number(Number {
7490 value: 2.0,
7491 units: NumericSuffix::Mm,
7492 }),
7493 },
7494 end: Point2d {
7495 x: Expr::Number(Number {
7496 value: 13.0,
7497 units: NumericSuffix::Mm,
7498 }),
7499 y: Expr::Number(Number {
7500 value: 14.0,
7501 units: NumericSuffix::Mm,
7502 }),
7503 },
7504 construction: None,
7505 };
7506 let segments = vec![ExistingSegmentCtor {
7507 id: line,
7508 ctor: SegmentCtor::Line(line_ctor),
7509 }];
7510 let (src_delta, scene_delta) = frontend
7511 .edit_segments(&mock_ctx, version, sketch_id, segments)
7512 .await
7513 .unwrap();
7514 assert_eq!(
7515 src_delta.text.as_str(),
7516 "sketch001 = sketch(on = XY) {
7517 line(start = [1mm, 2mm], end = [13mm, 14mm])
7518}
7519"
7520 );
7521 assert_eq!(scene_delta.new_objects, vec![]);
7522 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7523
7524 ctx.close().await;
7525 mock_ctx.close().await;
7526 }
7527
7528 #[tokio::test(flavor = "multi_thread")]
7529 async fn test_new_sketch_add_arc_edit_arc() {
7530 let program = Program::empty();
7531
7532 let mut frontend = FrontendState::new();
7533 frontend.program = program;
7534
7535 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7536 let mock_ctx = ExecutorContext::new_mock(None).await;
7537 let version = Version(0);
7538
7539 let sketch_args = SketchCtor {
7540 on: Plane::Default(PlaneName::Xy),
7541 };
7542 let (_src_delta, scene_delta, sketch_id) = frontend
7543 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7544 .await
7545 .unwrap();
7546 assert_eq!(sketch_id, ObjectId(1));
7547 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7548 let sketch_object = &scene_delta.new_graph.objects[1];
7549 assert_eq!(sketch_object.id, ObjectId(1));
7550 assert_eq!(
7551 sketch_object.kind,
7552 ObjectKind::Sketch(Sketch {
7553 args: SketchCtor {
7554 on: Plane::Default(PlaneName::Xy),
7555 },
7556 plane: ObjectId(0),
7557 segments: vec![],
7558 constraints: vec![],
7559 })
7560 );
7561 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7562
7563 let arc_ctor = ArcCtor {
7564 start: Point2d {
7565 x: Expr::Var(Number {
7566 value: 0.0,
7567 units: NumericSuffix::Mm,
7568 }),
7569 y: Expr::Var(Number {
7570 value: 0.0,
7571 units: NumericSuffix::Mm,
7572 }),
7573 },
7574 end: Point2d {
7575 x: Expr::Var(Number {
7576 value: 10.0,
7577 units: NumericSuffix::Mm,
7578 }),
7579 y: Expr::Var(Number {
7580 value: 10.0,
7581 units: NumericSuffix::Mm,
7582 }),
7583 },
7584 center: Point2d {
7585 x: Expr::Var(Number {
7586 value: 10.0,
7587 units: NumericSuffix::Mm,
7588 }),
7589 y: Expr::Var(Number {
7590 value: 0.0,
7591 units: NumericSuffix::Mm,
7592 }),
7593 },
7594 construction: None,
7595 };
7596 let segment = SegmentCtor::Arc(arc_ctor);
7597 let (src_delta, scene_delta) = frontend
7598 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7599 .await
7600 .unwrap();
7601 assert_eq!(
7602 src_delta.text.as_str(),
7603 "sketch001 = sketch(on = XY) {
7604 arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
7605}
7606"
7607 );
7608 assert_eq!(
7609 scene_delta.new_objects,
7610 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
7611 );
7612 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7613 assert_eq!(scene_object.id.0, i);
7614 }
7615 assert_eq!(scene_delta.new_graph.objects.len(), 6);
7616
7617 let arc = *scene_delta.new_objects.last().unwrap();
7619
7620 let arc_ctor = ArcCtor {
7621 start: Point2d {
7622 x: Expr::Var(Number {
7623 value: 1.0,
7624 units: NumericSuffix::Mm,
7625 }),
7626 y: Expr::Var(Number {
7627 value: 2.0,
7628 units: NumericSuffix::Mm,
7629 }),
7630 },
7631 end: Point2d {
7632 x: Expr::Var(Number {
7633 value: 13.0,
7634 units: NumericSuffix::Mm,
7635 }),
7636 y: Expr::Var(Number {
7637 value: 14.0,
7638 units: NumericSuffix::Mm,
7639 }),
7640 },
7641 center: Point2d {
7642 x: Expr::Var(Number {
7643 value: 13.0,
7644 units: NumericSuffix::Mm,
7645 }),
7646 y: Expr::Var(Number {
7647 value: 2.0,
7648 units: NumericSuffix::Mm,
7649 }),
7650 },
7651 construction: None,
7652 };
7653 let segments = vec![ExistingSegmentCtor {
7654 id: arc,
7655 ctor: SegmentCtor::Arc(arc_ctor),
7656 }];
7657 let (src_delta, scene_delta) = frontend
7658 .edit_segments(&mock_ctx, version, sketch_id, segments)
7659 .await
7660 .unwrap();
7661 assert_eq!(
7662 src_delta.text.as_str(),
7663 "sketch001 = sketch(on = XY) {
7664 arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
7665}
7666"
7667 );
7668 assert_eq!(scene_delta.new_objects, vec![]);
7669 assert_eq!(scene_delta.new_graph.objects.len(), 6);
7670
7671 ctx.close().await;
7672 mock_ctx.close().await;
7673 }
7674
7675 #[tokio::test(flavor = "multi_thread")]
7676 async fn test_new_sketch_add_circle_edit_circle() {
7677 let program = Program::empty();
7678
7679 let mut frontend = FrontendState::new();
7680 frontend.program = program;
7681
7682 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7683 let mock_ctx = ExecutorContext::new_mock(None).await;
7684 let version = Version(0);
7685
7686 let sketch_args = SketchCtor {
7687 on: Plane::Default(PlaneName::Xy),
7688 };
7689 let (_src_delta, _scene_delta, sketch_id) = frontend
7690 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7691 .await
7692 .unwrap();
7693
7694 let circle_ctor = CircleCtor {
7696 start: Point2d {
7697 x: Expr::Var(Number {
7698 value: 5.0,
7699 units: NumericSuffix::Mm,
7700 }),
7701 y: Expr::Var(Number {
7702 value: 0.0,
7703 units: NumericSuffix::Mm,
7704 }),
7705 },
7706 center: Point2d {
7707 x: Expr::Var(Number {
7708 value: 0.0,
7709 units: NumericSuffix::Mm,
7710 }),
7711 y: Expr::Var(Number {
7712 value: 0.0,
7713 units: NumericSuffix::Mm,
7714 }),
7715 },
7716 construction: None,
7717 };
7718 let segment = SegmentCtor::Circle(circle_ctor);
7719 let (src_delta, scene_delta) = frontend
7720 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7721 .await
7722 .unwrap();
7723 assert_eq!(
7724 src_delta.text.as_str(),
7725 "sketch001 = sketch(on = XY) {
7726 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7727}
7728"
7729 );
7730 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7732 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7733
7734 let circle = *scene_delta.new_objects.last().unwrap();
7735
7736 let circle_ctor = CircleCtor {
7738 start: Point2d {
7739 x: Expr::Var(Number {
7740 value: 10.0,
7741 units: NumericSuffix::Mm,
7742 }),
7743 y: Expr::Var(Number {
7744 value: 0.0,
7745 units: NumericSuffix::Mm,
7746 }),
7747 },
7748 center: Point2d {
7749 x: Expr::Var(Number {
7750 value: 3.0,
7751 units: NumericSuffix::Mm,
7752 }),
7753 y: Expr::Var(Number {
7754 value: 4.0,
7755 units: NumericSuffix::Mm,
7756 }),
7757 },
7758 construction: None,
7759 };
7760 let segments = vec![ExistingSegmentCtor {
7761 id: circle,
7762 ctor: SegmentCtor::Circle(circle_ctor),
7763 }];
7764 let (src_delta, scene_delta) = frontend
7765 .edit_segments(&mock_ctx, version, sketch_id, segments)
7766 .await
7767 .unwrap();
7768 assert_eq!(
7769 src_delta.text.as_str(),
7770 "sketch001 = sketch(on = XY) {
7771 circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
7772}
7773"
7774 );
7775 assert_eq!(scene_delta.new_objects, vec![]);
7776 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7777
7778 ctx.close().await;
7779 mock_ctx.close().await;
7780 }
7781
7782 #[tokio::test(flavor = "multi_thread")]
7783 async fn test_delete_circle() {
7784 let initial_source = "sketch001 = sketch(on = XY) {
7785 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7786}
7787";
7788
7789 let program = Program::parse(initial_source).unwrap().0.unwrap();
7790 let mut frontend = FrontendState::new();
7791
7792 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7793 let mock_ctx = ExecutorContext::new_mock(None).await;
7794 let version = Version(0);
7795
7796 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7797 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7798 let sketch_id = sketch_object.id;
7799 let sketch = expect_sketch(sketch_object);
7800
7801 assert_eq!(sketch.segments.len(), 3);
7803 let circle_id = sketch.segments[2];
7804
7805 let (src_delta, scene_delta) = frontend
7807 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
7808 .await
7809 .unwrap();
7810 assert_eq!(
7811 src_delta.text.as_str(),
7812 "sketch001 = sketch(on = XY) {
7813}
7814"
7815 );
7816 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7817 let new_sketch = expect_sketch(new_sketch_object);
7818 assert_eq!(new_sketch.segments.len(), 0);
7819
7820 ctx.close().await;
7821 mock_ctx.close().await;
7822 }
7823
7824 #[tokio::test(flavor = "multi_thread")]
7825 async fn test_edit_circle_via_point() {
7826 let initial_source = "sketch001 = sketch(on = XY) {
7827 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7828}
7829";
7830
7831 let program = Program::parse(initial_source).unwrap().0.unwrap();
7832 let mut frontend = FrontendState::new();
7833
7834 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7835 let mock_ctx = ExecutorContext::new_mock(None).await;
7836 let version = Version(0);
7837
7838 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7839 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7840 let sketch_id = sketch_object.id;
7841 let sketch = expect_sketch(sketch_object);
7842
7843 let circle_id = sketch
7845 .segments
7846 .iter()
7847 .copied()
7848 .find(|seg_id| {
7849 matches!(
7850 &frontend.scene_graph.objects[seg_id.0].kind,
7851 ObjectKind::Segment {
7852 segment: Segment::Circle(_)
7853 }
7854 )
7855 })
7856 .expect("Expected a circle segment in sketch");
7857 let circle_object = &frontend.scene_graph.objects[circle_id.0];
7858 let ObjectKind::Segment {
7859 segment: Segment::Circle(circle),
7860 } = &circle_object.kind
7861 else {
7862 panic!("Expected circle segment, got: {:?}", circle_object.kind);
7863 };
7864 let start_point_id = circle.start;
7865
7866 let segments = vec![ExistingSegmentCtor {
7868 id: start_point_id,
7869 ctor: SegmentCtor::Point(PointCtor {
7870 position: Point2d {
7871 x: Expr::Var(Number {
7872 value: 7.0,
7873 units: NumericSuffix::Mm,
7874 }),
7875 y: Expr::Var(Number {
7876 value: 1.0,
7877 units: NumericSuffix::Mm,
7878 }),
7879 },
7880 }),
7881 }];
7882 let (src_delta, _scene_delta) = frontend
7883 .edit_segments(&mock_ctx, version, sketch_id, segments)
7884 .await
7885 .unwrap();
7886 assert_eq!(
7887 src_delta.text.as_str(),
7888 "sketch001 = sketch(on = XY) {
7889 circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
7890}
7891"
7892 );
7893
7894 ctx.close().await;
7895 mock_ctx.close().await;
7896 }
7897
7898 #[tokio::test(flavor = "multi_thread")]
7899 async fn test_add_line_when_sketch_block_uses_variable() {
7900 let initial_source = "s = sketch(on = XY) {}
7901";
7902
7903 let program = Program::parse(initial_source).unwrap().0.unwrap();
7904
7905 let mut frontend = FrontendState::new();
7906
7907 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7908 let mock_ctx = ExecutorContext::new_mock(None).await;
7909 let version = Version(0);
7910
7911 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7912 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7913 let sketch_id = sketch_object.id;
7914
7915 let line_ctor = LineCtor {
7916 start: Point2d {
7917 x: Expr::Number(Number {
7918 value: 0.0,
7919 units: NumericSuffix::Mm,
7920 }),
7921 y: Expr::Number(Number {
7922 value: 0.0,
7923 units: NumericSuffix::Mm,
7924 }),
7925 },
7926 end: Point2d {
7927 x: Expr::Number(Number {
7928 value: 10.0,
7929 units: NumericSuffix::Mm,
7930 }),
7931 y: Expr::Number(Number {
7932 value: 10.0,
7933 units: NumericSuffix::Mm,
7934 }),
7935 },
7936 construction: None,
7937 };
7938 let segment = SegmentCtor::Line(line_ctor);
7939 let (src_delta, scene_delta) = frontend
7940 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7941 .await
7942 .unwrap();
7943 assert_eq!(
7944 src_delta.text.as_str(),
7945 "s = sketch(on = XY) {
7946 line(start = [0mm, 0mm], end = [10mm, 10mm])
7947}
7948"
7949 );
7950 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7951 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7952
7953 ctx.close().await;
7954 mock_ctx.close().await;
7955 }
7956
7957 #[tokio::test(flavor = "multi_thread")]
7958 async fn test_new_sketch_add_line_delete_sketch() {
7959 let program = Program::empty();
7960
7961 let mut frontend = FrontendState::new();
7962 frontend.program = program;
7963
7964 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7965 let mock_ctx = ExecutorContext::new_mock(None).await;
7966 let version = Version(0);
7967
7968 let sketch_args = SketchCtor {
7969 on: Plane::Default(PlaneName::Xy),
7970 };
7971 let (_src_delta, scene_delta, sketch_id) = frontend
7972 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7973 .await
7974 .unwrap();
7975 assert_eq!(sketch_id, ObjectId(1));
7976 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7977 let sketch_object = &scene_delta.new_graph.objects[1];
7978 assert_eq!(sketch_object.id, ObjectId(1));
7979 assert_eq!(
7980 sketch_object.kind,
7981 ObjectKind::Sketch(Sketch {
7982 args: SketchCtor {
7983 on: Plane::Default(PlaneName::Xy)
7984 },
7985 plane: ObjectId(0),
7986 segments: vec![],
7987 constraints: vec![],
7988 })
7989 );
7990 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7991
7992 let line_ctor = LineCtor {
7993 start: Point2d {
7994 x: Expr::Number(Number {
7995 value: 0.0,
7996 units: NumericSuffix::Mm,
7997 }),
7998 y: Expr::Number(Number {
7999 value: 0.0,
8000 units: NumericSuffix::Mm,
8001 }),
8002 },
8003 end: Point2d {
8004 x: Expr::Number(Number {
8005 value: 10.0,
8006 units: NumericSuffix::Mm,
8007 }),
8008 y: Expr::Number(Number {
8009 value: 10.0,
8010 units: NumericSuffix::Mm,
8011 }),
8012 },
8013 construction: None,
8014 };
8015 let segment = SegmentCtor::Line(line_ctor);
8016 let (src_delta, scene_delta) = frontend
8017 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8018 .await
8019 .unwrap();
8020 assert_eq!(
8021 src_delta.text.as_str(),
8022 "sketch001 = sketch(on = XY) {
8023 line(start = [0mm, 0mm], end = [10mm, 10mm])
8024}
8025"
8026 );
8027 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8028
8029 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8030 assert_eq!(src_delta.text.as_str(), "");
8031 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8032
8033 ctx.close().await;
8034 mock_ctx.close().await;
8035 }
8036
8037 #[tokio::test(flavor = "multi_thread")]
8038 async fn test_delete_sketch_when_sketch_block_uses_variable() {
8039 let initial_source = "s = sketch(on = XY) {}
8040";
8041
8042 let program = Program::parse(initial_source).unwrap().0.unwrap();
8043
8044 let mut frontend = FrontendState::new();
8045
8046 let ctx = ExecutorContext::new_with_engine(
8047 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
8048 Default::default(),
8049 );
8050 let version = Version(0);
8051
8052 frontend.hack_set_program(&ctx, program).await.unwrap();
8053 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8054 let sketch_id = sketch_object.id;
8055
8056 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8057 assert_eq!(src_delta.text.as_str(), "");
8058 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8059
8060 ctx.close().await;
8061 }
8062
8063 #[tokio::test(flavor = "multi_thread")]
8064 async fn test_delete_sketch_after_comment() {
8065 let initial_source = "sketch001 = sketch(on = XZ) {
8066}
8067";
8068
8069 let program = Program::parse(initial_source).unwrap().0.unwrap();
8070 let mut frontend = FrontendState::new();
8071
8072 let ctx = ExecutorContext::new_with_engine(
8073 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
8074 Default::default(),
8075 );
8076 let version = Version(0);
8077
8078 frontend.hack_set_program(&ctx, program).await.unwrap();
8079 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8080 let sketch_id = sketch_object.id;
8081 let original_source = sketch_object.source.clone();
8082
8083 let commented_source = "// test 1
8084sketch001 = sketch(on = XZ) {
8085}
8086";
8087 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8088 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8089
8090 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8091 assert_eq!(cached_sketch_object.source, original_source);
8092
8093 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8094 assert!(
8095 !src_delta.text.contains("sketch001"),
8096 "sketch was not deleted: {}",
8097 src_delta.text
8098 );
8099 assert_eq!(src_delta.text.as_str(), "// test 1\n");
8101 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8102
8103 ctx.close().await;
8104 }
8105
8106 #[tokio::test(flavor = "multi_thread")]
8107 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8108 let initial_source = "sketch001 = sketch(on = XZ) {
8109}
8110foo = 1
8111";
8112
8113 let program = Program::parse(initial_source).unwrap().0.unwrap();
8114 let mut frontend = FrontendState::new();
8115
8116 let ctx = ExecutorContext::new_with_engine(
8117 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
8118 Default::default(),
8119 );
8120 let version = Version(0);
8121
8122 frontend.hack_set_program(&ctx, program).await.unwrap();
8123 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8124 let sketch_id = sketch_object.id;
8125
8126 let commented_source = "// keep me
8127sketch001 = sketch(on = XZ) {
8128}
8129foo = 1
8130";
8131 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8132 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8133
8134 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8135 assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
8137
8138 ctx.close().await;
8139 }
8140
8141 #[tokio::test(flavor = "multi_thread")]
8142 async fn test_delete_segment_preserves_pre_comment() {
8143 let initial_source = "\
8144sketch(on = XY) {
8145 point(at = [var 1, var 2])
8146 // describe the middle point
8147 point(at = [var 3, var 4])
8148 point(at = [var 5, var 6])
8149}
8150";
8151
8152 let program = Program::parse(initial_source).unwrap().0.unwrap();
8153 let mut frontend = FrontendState::new();
8154
8155 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8156 let mock_ctx = ExecutorContext::new_mock(None).await;
8157 let version = Version(0);
8158
8159 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8160 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8161 let sketch_id = sketch_object.id;
8162 let sketch = expect_sketch(sketch_object);
8163
8164 let middle_point_id = *sketch.segments.get(1).unwrap();
8165
8166 let (src_delta, _scene_delta) = frontend
8167 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8168 .await
8169 .unwrap();
8170 assert_eq!(
8173 src_delta.text.as_str(),
8174 "\
8175sketch(on = XY) {
8176 point(at = [var 1, var 2])
8177 // describe the middle point
8178 point(at = [var 5, var 6])
8179}
8180"
8181 );
8182
8183 ctx.close().await;
8184 mock_ctx.close().await;
8185 }
8186
8187 #[tokio::test(flavor = "multi_thread")]
8188 async fn test_delete_last_segment_preserves_pre_comment() {
8189 let initial_source = "\
8190sketch(on = XY) {
8191 point(at = [var 1, var 2])
8192 // describe the trailing point
8193 point(at = [var 3, var 4])
8194}
8195";
8196
8197 let program = Program::parse(initial_source).unwrap().0.unwrap();
8198 let mut frontend = FrontendState::new();
8199
8200 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8201 let mock_ctx = ExecutorContext::new_mock(None).await;
8202 let version = Version(0);
8203
8204 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8205 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8206 let sketch_id = sketch_object.id;
8207 let sketch = expect_sketch(sketch_object);
8208
8209 let last_point_id = *sketch.segments.last().unwrap();
8210
8211 let (src_delta, _scene_delta) = frontend
8212 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8213 .await
8214 .unwrap();
8215 assert_eq!(
8218 src_delta.text.as_str(),
8219 "\
8220sketch(on = XY) {
8221 point(at = [var 1, var 2])
8222 // describe the trailing point
8223}
8224"
8225 );
8226
8227 ctx.close().await;
8228 mock_ctx.close().await;
8229 }
8230
8231 #[tokio::test(flavor = "multi_thread")]
8232 async fn test_delete_segment_drops_inline_trailing_comment() {
8233 let initial_source = "\
8234sketch(on = XY) {
8235 point(at = [var 1, var 2])
8236 point(at = [var 3, var 4]) // same-line note that gets dropped
8237 point(at = [var 5, var 6])
8238}
8239";
8240
8241 let program = Program::parse(initial_source).unwrap().0.unwrap();
8242 let mut frontend = FrontendState::new();
8243
8244 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8245 let mock_ctx = ExecutorContext::new_mock(None).await;
8246 let version = Version(0);
8247
8248 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8249 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8250 let sketch_id = sketch_object.id;
8251 let sketch = expect_sketch(sketch_object);
8252
8253 let middle_point_id = *sketch.segments.get(1).unwrap();
8254
8255 let (src_delta, _scene_delta) = frontend
8256 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8257 .await
8258 .unwrap();
8259 assert!(
8261 !src_delta.text.contains("same-line note"),
8262 "inline comment should have been removed: {}",
8263 src_delta.text
8264 );
8265
8266 ctx.close().await;
8267 mock_ctx.close().await;
8268 }
8269
8270 #[tokio::test(flavor = "multi_thread")]
8271 async fn test_delete_segments_preserves_block_comments_across_positions() {
8272 let initial_source = "\
8280sketch(on = XY) {
8281 /* above first - moves to middle */
8282 point(at = [var 1, var 2]) /* same-line on first - dropped */
8283 /* above middle - stays */
8284 point(at = [var 3, var 4])
8285 /* above last - moves to trailing meta */
8286 point(at = [var 5, var 6])
8287}
8288";
8289
8290 let program = Program::parse(initial_source).unwrap().0.unwrap();
8291 let mut frontend = FrontendState::new();
8292
8293 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8294 let mock_ctx = ExecutorContext::new_mock(None).await;
8295 let version = Version(0);
8296
8297 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8298 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8299 let sketch_id = sketch_object.id;
8300 let sketch = expect_sketch(sketch_object);
8301
8302 let first_point_id = *sketch.segments.first().unwrap();
8303 let last_point_id = *sketch.segments.last().unwrap();
8304
8305 let (src_delta, _scene_delta) = frontend
8306 .delete_objects(
8307 &mock_ctx,
8308 version,
8309 sketch_id,
8310 Vec::new(),
8311 vec![first_point_id, last_point_id],
8312 )
8313 .await
8314 .unwrap();
8315 assert_eq!(
8316 src_delta.text.as_str(),
8317 "\
8318sketch(on = XY) {
8319 /* above first - moves to middle */
8320 /* above middle - stays */
8321 point(at = [var 3, var 4])
8322 /* above last - moves to trailing meta */
8323}
8324"
8325 );
8326
8327 ctx.close().await;
8328 mock_ctx.close().await;
8329 }
8330
8331 #[tokio::test(flavor = "multi_thread")]
8332 async fn test_edit_line_when_editing_its_start_point() {
8333 let initial_source = "\
8334sketch(on = XY) {
8335 line(start = [var 1, var 2], end = [var 3, var 4])
8336}
8337";
8338
8339 let program = Program::parse(initial_source).unwrap().0.unwrap();
8340
8341 let mut frontend = FrontendState::new();
8342
8343 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8344 let mock_ctx = ExecutorContext::new_mock(None).await;
8345 let version = Version(0);
8346
8347 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8348 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8349 let sketch_id = sketch_object.id;
8350 let sketch = expect_sketch(sketch_object);
8351
8352 let point_id = *sketch.segments.first().unwrap();
8353
8354 let point_ctor = PointCtor {
8355 position: Point2d {
8356 x: Expr::Var(Number {
8357 value: 5.0,
8358 units: NumericSuffix::Inch,
8359 }),
8360 y: Expr::Var(Number {
8361 value: 6.0,
8362 units: NumericSuffix::Inch,
8363 }),
8364 },
8365 };
8366 let segments = vec![ExistingSegmentCtor {
8367 id: point_id,
8368 ctor: SegmentCtor::Point(point_ctor),
8369 }];
8370 let (src_delta, scene_delta) = frontend
8371 .edit_segments(&mock_ctx, version, sketch_id, segments)
8372 .await
8373 .unwrap();
8374 assert_eq!(
8375 src_delta.text.as_str(),
8376 "\
8377sketch(on = XY) {
8378 line(start = [var 5in, var 6in], end = [var 3, var 4])
8379}
8380"
8381 );
8382 assert_eq!(scene_delta.new_objects, vec![]);
8383 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8384
8385 ctx.close().await;
8386 mock_ctx.close().await;
8387 }
8388
8389 #[tokio::test(flavor = "multi_thread")]
8390 async fn test_edit_line_when_editing_its_end_point() {
8391 let initial_source = "\
8392sketch(on = XY) {
8393 line(start = [var 1, var 2], end = [var 3, var 4])
8394}
8395";
8396
8397 let program = Program::parse(initial_source).unwrap().0.unwrap();
8398
8399 let mut frontend = FrontendState::new();
8400
8401 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8402 let mock_ctx = ExecutorContext::new_mock(None).await;
8403 let version = Version(0);
8404
8405 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8406 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8407 let sketch_id = sketch_object.id;
8408 let sketch = expect_sketch(sketch_object);
8409 let point_id = *sketch.segments.get(1).unwrap();
8410
8411 let point_ctor = PointCtor {
8412 position: Point2d {
8413 x: Expr::Var(Number {
8414 value: 5.0,
8415 units: NumericSuffix::Inch,
8416 }),
8417 y: Expr::Var(Number {
8418 value: 6.0,
8419 units: NumericSuffix::Inch,
8420 }),
8421 },
8422 };
8423 let segments = vec![ExistingSegmentCtor {
8424 id: point_id,
8425 ctor: SegmentCtor::Point(point_ctor),
8426 }];
8427 let (src_delta, scene_delta) = frontend
8428 .edit_segments(&mock_ctx, version, sketch_id, segments)
8429 .await
8430 .unwrap();
8431 assert_eq!(
8432 src_delta.text.as_str(),
8433 "\
8434sketch(on = XY) {
8435 line(start = [var 1, var 2], end = [var 5in, var 6in])
8436}
8437"
8438 );
8439 assert_eq!(scene_delta.new_objects, vec![]);
8440 assert_eq!(
8441 scene_delta.new_graph.objects.len(),
8442 5,
8443 "{:#?}",
8444 scene_delta.new_graph.objects
8445 );
8446
8447 ctx.close().await;
8448 mock_ctx.close().await;
8449 }
8450
8451 #[tokio::test(flavor = "multi_thread")]
8452 async fn test_edit_line_with_coincident_feedback() {
8453 let initial_source = "\
8454sketch(on = XY) {
8455 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8456 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8457 fixed([line1.start, [0, 0]])
8458 coincident([line1.end, line2.start])
8459 equalLength([line1, line2])
8460}
8461";
8462
8463 let program = Program::parse(initial_source).unwrap().0.unwrap();
8464
8465 let mut frontend = FrontendState::new();
8466
8467 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8468 let mock_ctx = ExecutorContext::new_mock(None).await;
8469 let version = Version(0);
8470
8471 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8472 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8473 let sketch_id = sketch_object.id;
8474 let sketch = expect_sketch(sketch_object);
8475 let line2_end_id = *sketch.segments.get(4).unwrap();
8476
8477 let segments = vec![ExistingSegmentCtor {
8478 id: line2_end_id,
8479 ctor: SegmentCtor::Point(PointCtor {
8480 position: Point2d {
8481 x: Expr::Var(Number {
8482 value: 9.0,
8483 units: NumericSuffix::None,
8484 }),
8485 y: Expr::Var(Number {
8486 value: 10.0,
8487 units: NumericSuffix::None,
8488 }),
8489 },
8490 }),
8491 }];
8492 let (src_delta, scene_delta) = frontend
8493 .edit_segments(&mock_ctx, version, sketch_id, segments)
8494 .await
8495 .unwrap();
8496 assert_eq!(
8497 src_delta.text.as_str(),
8498 "\
8499sketch(on = XY) {
8500 line1 = line(start = [var 0, var 0], end = [var 4.14, var 5.32])
8501 line2 = line(start = [var 4.14, var 5.32], end = [var 9, var 10])
8502 fixed([line1.start, [0, 0]])
8503 coincident([line1.end, line2.start])
8504 equalLength([line1, line2])
8505}
8506"
8507 );
8508 assert_eq!(
8509 scene_delta.new_graph.objects.len(),
8510 11,
8511 "{:#?}",
8512 scene_delta.new_graph.objects
8513 );
8514
8515 ctx.close().await;
8516 mock_ctx.close().await;
8517 }
8518
8519 #[tokio::test(flavor = "multi_thread")]
8520 async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
8521 let initial_source = "\
8522sketch(on = XY) {
8523 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8524 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8525 fixed([line1.start, [0, 0]])
8526 coincident([line1.end, line2.start])
8527 equalLength([line1, line2])
8528}
8529";
8530
8531 let program = Program::parse(initial_source).unwrap().0.unwrap();
8532 let mut frontend = FrontendState::new();
8533 let mock_ctx = ExecutorContext::new_mock(None).await;
8534 let version = Version(0);
8535
8536 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8537 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8538 let sketch_id = sketch_object.id;
8539 let sketch = expect_sketch(sketch_object);
8540 let line2_end_id = *sketch.segments.get(4).unwrap();
8541
8542 let segments = vec![ExistingSegmentCtor {
8543 id: line2_end_id,
8544 ctor: SegmentCtor::Point(PointCtor {
8545 position: Point2d {
8546 x: Expr::Var(Number {
8547 value: 9.0,
8548 units: NumericSuffix::None,
8549 }),
8550 y: Expr::Var(Number {
8551 value: 10.0,
8552 units: NumericSuffix::None,
8553 }),
8554 },
8555 }),
8556 }];
8557 let (edited_source, _) = frontend
8558 .edit_segments(&mock_ctx, version, sketch_id, segments)
8559 .await
8560 .unwrap();
8561
8562 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8563 assert_eq!(mock_source.text, edited_source.text);
8564
8565 mock_ctx.close().await;
8566 }
8567
8568 #[tokio::test(flavor = "multi_thread")]
8571 async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
8572 let initial_source = "\
8573sketch(on = XY) {
8574 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8575 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8576 fixed([line1.start, [0, 0]])
8577 coincident([line1.end, line2.start])
8578 equalLength([line1, line2])
8579}
8580";
8581
8582 let program = Program::parse(initial_source).unwrap().0.unwrap();
8583 let mut frontend = FrontendState::new();
8584 let mock_ctx = ExecutorContext::new_mock(None).await;
8585 let version = Version(0);
8586
8587 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8588 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8589 let sketch_id = sketch_object.id;
8590 let sketch = expect_sketch(sketch_object);
8591 let line2_end_id = *sketch.segments.get(4).unwrap();
8592
8593 let segments = vec![ExistingSegmentCtor {
8594 id: line2_end_id,
8595 ctor: SegmentCtor::Point(PointCtor {
8596 position: Point2d {
8597 x: Expr::Var(Number {
8598 value: 9.0,
8599 units: NumericSuffix::None,
8600 }),
8601 y: Expr::Var(Number {
8602 value: 10.0,
8603 units: NumericSuffix::None,
8604 }),
8605 },
8606 }),
8607 }];
8608 let (preview_source, preview_delta) = frontend
8609 .edit_segments_with_options(
8610 &mock_ctx,
8611 version,
8612 sketch_id,
8613 segments,
8614 EditSegmentsOptions {
8615 anchor_segment_ids: Some(vec![line2_end_id]),
8616 drag_anchors: Vec::new(),
8617 commit_solved_initial_guesses: false,
8618 },
8619 )
8620 .await
8621 .unwrap();
8622
8623 assert!(
8624 !preview_delta.exec_outcome.var_solutions.is_empty(),
8625 "preview solve should still solve and return geometry feedback"
8626 );
8627 assert!(
8628 preview_source
8629 .text
8630 .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
8631 );
8632 assert!(
8633 preview_source
8634 .text
8635 .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
8636 );
8637
8638 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8639 assert_eq!(mock_source.text, preview_source.text);
8640
8641 mock_ctx.close().await;
8642 }
8643
8644 #[tokio::test(flavor = "multi_thread")]
8645 async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
8646 let initial_source = "\
8647sketch(on = XY) {
8648 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
8649}
8650";
8651
8652 let program = Program::parse(initial_source).unwrap().0.unwrap();
8653 let mut frontend = FrontendState::new();
8654 let mock_ctx = ExecutorContext::new_mock(None).await;
8655 let version = Version(0);
8656
8657 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8658 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8659 let sketch_id = sketch_object.id;
8660 let sketch = expect_sketch(sketch_object);
8661 let line_end_id = *sketch.segments.get(1).unwrap();
8662
8663 let constraint = Constraint::Fixed(Fixed {
8664 points: vec![FixedPoint {
8665 point: line_end_id,
8666 position: Point2d {
8667 x: Number {
8668 value: 20.0,
8669 units: NumericSuffix::Mm,
8670 },
8671 y: Number {
8672 value: 0.0,
8673 units: NumericSuffix::Mm,
8674 },
8675 },
8676 }],
8677 });
8678 let (constraint_source, _) = frontend
8679 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8680 .await
8681 .unwrap();
8682
8683 assert!(
8684 constraint_source
8685 .text
8686 .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
8687 "{}",
8688 constraint_source.text
8689 );
8690 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8691 assert_eq!(mock_source.text, constraint_source.text);
8692
8693 mock_ctx.close().await;
8694 }
8695
8696 #[test]
8697 fn test_no_solver_feedback_preserves_original_source() {
8698 let initial_source = "\
8699@settings(defaultLengthUnit = in, kclVersion = 2.0)
8700cylinder = startSketchOn(XY)
8701 |> circle(center= [0, 0], radius= 22)
8702 |> extrude(length = 14)
8703";
8704 let mut frontend = FrontendState::new();
8705 frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
8706 let outcome = ExecOutcome {
8707 variables: Default::default(),
8708 operations: Default::default(),
8709 artifact_graph: Default::default(),
8710 scene_objects: Default::default(),
8711 source_range_to_object: Default::default(),
8712 var_solutions: Default::default(),
8713 issues: Default::default(),
8714 filenames: Default::default(),
8715 default_planes: Default::default(),
8716 };
8717
8718 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8719
8720 assert_eq!(source_delta.text, initial_source);
8721 }
8722
8723 #[tokio::test(flavor = "multi_thread")]
8726 async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
8727 let initial_source = "\
8728sketch(on = XY) {
8729 point1 = point(at = [var 0mm, var 0mm])
8730 point2 = point(at = [var 0mm, var 0mm])
8731 coincident([point1, point2])
8732}
8733";
8734
8735 let program = Program::parse(initial_source).unwrap().0.unwrap();
8736 let mut frontend = FrontendState::new();
8737 let mock_ctx = ExecutorContext::new_mock(None).await;
8738 let version = Version(0);
8739
8740 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8741 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8742 let sketch_id = sketch_object.id;
8743 let sketch = expect_sketch(sketch_object);
8744 let point1_id = sketch.segments[0];
8745 let point2_id = sketch.segments[1];
8746
8747 let segments = vec![
8748 ExistingSegmentCtor {
8749 id: point1_id,
8750 ctor: SegmentCtor::Point(PointCtor {
8751 position: point_expr_mm(10.0, 0.0),
8752 }),
8753 },
8754 ExistingSegmentCtor {
8755 id: point2_id,
8756 ctor: SegmentCtor::Point(PointCtor {
8757 position: point_expr_mm(100.0, 0.0),
8758 }),
8759 },
8760 ];
8761 let (_, scene_delta) = frontend
8762 .edit_segments_with_options(
8763 &mock_ctx,
8764 version,
8765 sketch_id,
8766 segments,
8767 EditSegmentsOptions {
8768 anchor_segment_ids: Some(vec![point1_id]),
8769 drag_anchors: Vec::new(),
8770 commit_solved_initial_guesses: true,
8771 },
8772 )
8773 .await
8774 .unwrap();
8775
8776 assert_point_position_close(
8777 point_position(&scene_delta.new_graph, point1_id),
8778 point_number_mm(10.0, 0.0),
8779 );
8780 assert_point_position_close(
8781 point_position(&scene_delta.new_graph, point2_id),
8782 point_number_mm(10.0, 0.0),
8783 );
8784
8785 mock_ctx.close().await;
8786 }
8787
8788 fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
8791 use std::cell::RefCell;
8792 struct Collector {
8793 target: f64,
8794 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
8795 }
8796 impl<'a> crate::walk::Visitor<'a> for &Collector {
8797 type Error = crate::front::Error;
8798 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
8799 if let crate::walk::Node::SketchVar(sketch_var) = node
8800 && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
8801 && (initial.value - self.target).abs() < 1e-9
8802 {
8803 self.out
8804 .borrow_mut()
8805 .push((SourceRange::from(initial.as_ref()), node_path.clone()));
8806 }
8807 for child in node.children().iter() {
8808 if !child.visit(*self)? {
8809 return Ok(false);
8810 }
8811 }
8812 Ok(true)
8813 }
8814 }
8815 let collector = Collector {
8816 target: value,
8817 out: Default::default(),
8818 };
8819 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
8820 collector.out.into_inner()
8821 }
8822
8823 fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
8826 use std::cell::RefCell;
8827 struct Collector {
8828 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
8829 }
8830 impl<'a> crate::walk::Visitor<'a> for &Collector {
8831 type Error = crate::front::Error;
8832 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
8833 if let crate::walk::Node::SketchVar(sketch_var) = node
8834 && let Some(node_path) = &sketch_var.node_path
8835 {
8836 self.out
8837 .borrow_mut()
8838 .push((SourceRange::from(sketch_var), node_path.clone()));
8839 }
8840 for child in node.children().iter() {
8841 if !child.visit(*self)? {
8842 return Ok(false);
8843 }
8844 }
8845 Ok(true)
8846 }
8847 }
8848 let collector = Collector {
8849 out: Default::default(),
8850 };
8851 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
8852 collector.out.into_inner()
8853 }
8854
8855 fn empty_exec_outcome_with_var_solutions(
8856 var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
8857 ) -> ExecOutcome {
8858 ExecOutcome {
8859 variables: Default::default(),
8860 operations: Default::default(),
8861 artifact_graph: Default::default(),
8862 scene_objects: Default::default(),
8863 source_range_to_object: Default::default(),
8864 var_solutions,
8865 issues: Default::default(),
8866 filenames: Default::default(),
8867 default_planes: Default::default(),
8868 }
8869 }
8870
8871 #[test]
8875 fn test_commit_var_solution_by_node_path_updates_sketch_var() {
8876 let initial_source = "\
8877sketch(on = XY) {
8878 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
8879}
8880";
8881 let program = Program::parse(initial_source).unwrap().0.unwrap();
8882 let matches = collect_sketch_var_literals_with_value(&program, 10.0);
8883 assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
8884 let (literal_range, node_path) = matches.into_iter().next().unwrap();
8885
8886 let mut frontend = FrontendState::new();
8887 frontend.program = program;
8888
8889 let outcome = empty_exec_outcome_with_var_solutions(vec![(
8890 literal_range,
8891 Some(node_path),
8892 Number {
8893 value: 25.0,
8894 units: NumericSuffix::Mm,
8895 },
8896 )]);
8897
8898 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8899
8900 assert_eq!(
8901 source_delta.text,
8902 "\
8903sketch(on = XY) {
8904 line1 = line(start = [var 0, var 0], end = [var 25mm, var 0])
8905}
8906",
8907 );
8908 }
8909
8910 #[test]
8917 fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
8918 let compact_source = "\
8919sketch(on = XY) {
8920 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
8921}
8922";
8923 let padded_source = "\
8924// added comment\n// added comment\n\nsketch(on = XY) {
8925 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
8926}
8927";
8928 let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
8929 let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
8930
8931 let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
8932 .into_iter()
8933 .next()
8934 .expect("expected `var 10mm` in compact source");
8935 let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
8936 .into_iter()
8937 .next()
8938 .expect("expected `var 10mm` in padded source");
8939
8940 assert_ne!(
8941 compact_match.0, padded_match.0,
8942 "byte offsets must differ for this test to be meaningful"
8943 );
8944 assert_eq!(
8945 compact_match.1, padded_match.1,
8946 "node paths must agree across whitespace; that's the whole point of NodePath",
8947 );
8948
8949 let mut frontend = FrontendState::new();
8950 frontend.program = padded_program;
8951
8952 let outcome = empty_exec_outcome_with_var_solutions(vec![(
8954 compact_match.0,
8955 Some(compact_match.1),
8956 Number {
8957 value: 30.0,
8958 units: NumericSuffix::Mm,
8959 },
8960 )]);
8961
8962 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8963
8964 assert_eq!(
8965 source_delta.text,
8966 "\
8967// added comment
8968// added comment
8969
8970sketch(on = XY) {
8971 line1 = line(start = [var 0, var 0], end = [var 30mm, var 0])
8972}
8973",
8974 );
8975 }
8976
8977 #[test]
8981 fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
8982 let initial_source = "\
8983sketch(on = XY) {
8984 line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
8985}
8986";
8987 let program = Program::parse(initial_source).unwrap().0.unwrap();
8988
8989 let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
8990 .into_iter()
8991 .next()
8992 .expect("expected `var 10mm`");
8993 let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
8994 .into_iter()
8995 .next()
8996 .expect("expected `var 20mm`");
8997
8998 let mut frontend = FrontendState::new();
8999 frontend.program = program;
9000
9001 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9003 var_20.0,
9004 Some(var_10.1),
9005 Number {
9006 value: 33.0,
9007 units: NumericSuffix::Mm,
9008 },
9009 )]);
9010
9011 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9012
9013 assert_eq!(
9014 source_delta.text,
9015 "\
9016sketch(on = XY) {
9017 line1 = line(start = [var 33mm, var 0mm], end = [var 20mm, var 0mm])
9018}
9019",
9020 );
9021 }
9022
9023 #[test]
9030 fn test_commit_var_solution_writes_back_into_bare_var() {
9031 let initial_source = "\
9032@settings(experimentalFeatures = allow, kclVersion = 2.0)
9033sketch(on = XY) {
9034 line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9035}
9036";
9037 let program = Program::parse(initial_source).unwrap().0.unwrap();
9038
9039 let bare = collect_all_sketch_vars(&program)
9042 .into_iter()
9043 .find(|(range, _)| {
9044 range.end() - range.start() == 3
9046 })
9047 .expect("expected at least one bare `var`");
9048
9049 let mut frontend = FrontendState::new();
9050 frontend.program = program;
9051
9052 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9053 bare.0,
9054 Some(bare.1),
9055 Number {
9056 value: 7.0,
9057 units: NumericSuffix::Mm,
9058 },
9059 )]);
9060
9061 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9062
9063 assert_eq!(
9067 source_delta.text,
9068 "\
9069@settings(experimentalFeatures = allow, kclVersion = 2.0)
9070
9071sketch(on = XY) {
9072 line1 = line(start = [var 7mm, var 0mm], end = [var 10mm, var 0])
9073}
9074",
9075 );
9076 }
9077
9078 #[tokio::test(flavor = "multi_thread")]
9079 async fn test_delete_point_without_var() {
9080 let initial_source = "\
9081sketch(on = XY) {
9082 point(at = [var 1, var 2])
9083 point(at = [var 3, var 4])
9084 point(at = [var 5, var 6])
9085}
9086";
9087
9088 let program = Program::parse(initial_source).unwrap().0.unwrap();
9089
9090 let mut frontend = FrontendState::new();
9091
9092 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9093 let mock_ctx = ExecutorContext::new_mock(None).await;
9094 let version = Version(0);
9095
9096 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9097 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9098 let sketch_id = sketch_object.id;
9099 let sketch = expect_sketch(sketch_object);
9100
9101 let point_id = *sketch.segments.get(1).unwrap();
9102
9103 let (src_delta, scene_delta) = frontend
9104 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9105 .await
9106 .unwrap();
9107 assert_eq!(
9108 src_delta.text.as_str(),
9109 "\
9110sketch(on = XY) {
9111 point(at = [var 1, var 2])
9112 point(at = [var 5, var 6])
9113}
9114"
9115 );
9116 assert_eq!(scene_delta.new_objects, vec![]);
9117 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9118
9119 ctx.close().await;
9120 mock_ctx.close().await;
9121 }
9122
9123 #[tokio::test(flavor = "multi_thread")]
9124 async fn test_delete_point_with_var() {
9125 let initial_source = "\
9126sketch(on = XY) {
9127 point(at = [var 1, var 2])
9128 point1 = point(at = [var 3, var 4])
9129 point(at = [var 5, var 6])
9130}
9131";
9132
9133 let program = Program::parse(initial_source).unwrap().0.unwrap();
9134
9135 let mut frontend = FrontendState::new();
9136
9137 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9138 let mock_ctx = ExecutorContext::new_mock(None).await;
9139 let version = Version(0);
9140
9141 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9142 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9143 let sketch_id = sketch_object.id;
9144 let sketch = expect_sketch(sketch_object);
9145
9146 let point_id = *sketch.segments.get(1).unwrap();
9147
9148 let (src_delta, scene_delta) = frontend
9149 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9150 .await
9151 .unwrap();
9152 assert_eq!(
9153 src_delta.text.as_str(),
9154 "\
9155sketch(on = XY) {
9156 point(at = [var 1, var 2])
9157 point(at = [var 5, var 6])
9158}
9159"
9160 );
9161 assert_eq!(scene_delta.new_objects, vec![]);
9162 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9163
9164 ctx.close().await;
9165 mock_ctx.close().await;
9166 }
9167
9168 #[tokio::test(flavor = "multi_thread")]
9169 async fn test_delete_multiple_points() {
9170 let initial_source = "\
9171sketch(on = XY) {
9172 point(at = [var 1, var 2])
9173 point1 = point(at = [var 3, var 4])
9174 point(at = [var 5, var 6])
9175}
9176";
9177
9178 let program = Program::parse(initial_source).unwrap().0.unwrap();
9179
9180 let mut frontend = FrontendState::new();
9181
9182 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9183 let mock_ctx = ExecutorContext::new_mock(None).await;
9184 let version = Version(0);
9185
9186 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9187 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9188 let sketch_id = sketch_object.id;
9189
9190 let sketch = expect_sketch(sketch_object);
9191
9192 let point1_id = *sketch.segments.first().unwrap();
9193 let point2_id = *sketch.segments.get(1).unwrap();
9194
9195 let (src_delta, scene_delta) = frontend
9196 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9197 .await
9198 .unwrap();
9199 assert_eq!(
9200 src_delta.text.as_str(),
9201 "\
9202sketch(on = XY) {
9203 point(at = [var 5, var 6])
9204}
9205"
9206 );
9207 assert_eq!(scene_delta.new_objects, vec![]);
9208 assert_eq!(scene_delta.new_graph.objects.len(), 3);
9209
9210 ctx.close().await;
9211 mock_ctx.close().await;
9212 }
9213
9214 #[tokio::test(flavor = "multi_thread")]
9215 async fn test_delete_coincident_constraint() {
9216 let initial_source = "\
9217sketch(on = XY) {
9218 point1 = point(at = [var 1, var 2])
9219 point2 = point(at = [var 3, var 4])
9220 coincident([point1, point2])
9221 point(at = [var 5, var 6])
9222}
9223";
9224
9225 let program = Program::parse(initial_source).unwrap().0.unwrap();
9226
9227 let mut frontend = FrontendState::new();
9228
9229 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9230 let mock_ctx = ExecutorContext::new_mock(None).await;
9231 let version = Version(0);
9232
9233 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9234 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9235 let sketch_id = sketch_object.id;
9236 let sketch = expect_sketch(sketch_object);
9237
9238 let coincident_id = *sketch.constraints.first().unwrap();
9239
9240 let (src_delta, scene_delta) = frontend
9241 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9242 .await
9243 .unwrap();
9244 assert_eq!(
9245 src_delta.text.as_str(),
9246 "\
9247sketch(on = XY) {
9248 point1 = point(at = [var 1, var 2])
9249 point2 = point(at = [var 3, var 4])
9250 point(at = [var 5, var 6])
9251}
9252"
9253 );
9254 assert_eq!(scene_delta.new_objects, vec![]);
9255 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9256
9257 ctx.close().await;
9258 mock_ctx.close().await;
9259 }
9260
9261 #[tokio::test(flavor = "multi_thread")]
9262 async fn test_delete_line_cascades_to_coincident_constraint() {
9263 let initial_source = "\
9264sketch(on = XY) {
9265 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9266 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9267 coincident([line1.end, line2.start])
9268}
9269";
9270
9271 let program = Program::parse(initial_source).unwrap().0.unwrap();
9272
9273 let mut frontend = FrontendState::new();
9274
9275 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9276 let mock_ctx = ExecutorContext::new_mock(None).await;
9277 let version = Version(0);
9278
9279 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9280 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9281 let sketch_id = sketch_object.id;
9282 let sketch = expect_sketch(sketch_object);
9283 let line_id = *sketch.segments.get(5).unwrap();
9284
9285 let (src_delta, scene_delta) = frontend
9286 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9287 .await
9288 .unwrap();
9289 assert_eq!(
9290 src_delta.text.as_str(),
9291 "\
9292sketch(on = XY) {
9293 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9294}
9295"
9296 );
9297 assert_eq!(
9298 scene_delta.new_graph.objects.len(),
9299 5,
9300 "{:#?}",
9301 scene_delta.new_graph.objects
9302 );
9303
9304 ctx.close().await;
9305 mock_ctx.close().await;
9306 }
9307
9308 #[tokio::test(flavor = "multi_thread")]
9309 async fn test_delete_line_cascades_to_distance_constraint() {
9310 let initial_source = "\
9311sketch(on = XY) {
9312 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9313 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9314 distance([line1.end, line2.start]) == 10mm
9315}
9316";
9317
9318 let program = Program::parse(initial_source).unwrap().0.unwrap();
9319
9320 let mut frontend = FrontendState::new();
9321
9322 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9323 let mock_ctx = ExecutorContext::new_mock(None).await;
9324 let version = Version(0);
9325
9326 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9327 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9328 let sketch_id = sketch_object.id;
9329 let sketch = expect_sketch(sketch_object);
9330 let line_id = *sketch.segments.get(5).unwrap();
9331
9332 let (src_delta, scene_delta) = frontend
9333 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9334 .await
9335 .unwrap();
9336 assert_eq!(
9337 src_delta.text.as_str(),
9338 "\
9339sketch(on = XY) {
9340 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9341}
9342"
9343 );
9344 assert_eq!(
9345 scene_delta.new_graph.objects.len(),
9346 5,
9347 "{:#?}",
9348 scene_delta.new_graph.objects
9349 );
9350
9351 ctx.close().await;
9352 mock_ctx.close().await;
9353 }
9354
9355 #[tokio::test(flavor = "multi_thread")]
9356 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9357 let initial_source = "\
9358sketch(on = XY) {
9359 point1 = point(at = [var 1, var 2])
9360 point2 = point(at = [var 3, var 4])
9361 horizontalDistance([point1, point2]) == 10mm
9362}
9363";
9364
9365 let program = Program::parse(initial_source).unwrap().0.unwrap();
9366
9367 let mut frontend = FrontendState::new();
9368
9369 let mock_ctx = ExecutorContext::new_mock(None).await;
9370 let version = Version(0);
9371
9372 frontend.program = program.clone();
9373 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9374 frontend.update_state_after_exec(outcome, true);
9375 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9376 let sketch_id = sketch_object.id;
9377 let sketch = expect_sketch(sketch_object);
9378 let point2_id = *sketch.segments.get(1).unwrap();
9379
9380 let (src_delta, scene_delta) = frontend
9381 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9382 .await
9383 .unwrap();
9384 assert_eq!(
9385 src_delta.text.as_str(),
9386 "\
9387sketch(on = XY) {
9388 point1 = point(at = [var 1, var 2])
9389}
9390"
9391 );
9392 assert_eq!(
9393 scene_delta.new_graph.objects.len(),
9394 3,
9395 "{:#?}",
9396 scene_delta.new_graph.objects
9397 );
9398
9399 mock_ctx.close().await;
9400 }
9401
9402 #[tokio::test(flavor = "multi_thread")]
9403 async fn test_delete_line_cascades_to_fixed_constraint() {
9404 let initial_source = "\
9405sketch(on = XY) {
9406 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9407 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9408 fixed([line1.start, [0, 0]])
9409}
9410";
9411
9412 let program = Program::parse(initial_source).unwrap().0.unwrap();
9413
9414 let mut frontend = FrontendState::new();
9415
9416 let mock_ctx = ExecutorContext::new_mock(None).await;
9417 let version = Version(0);
9418
9419 frontend.program = program.clone();
9420 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9421 frontend.update_state_after_exec(outcome, true);
9422 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9423 let sketch_id = sketch_object.id;
9424 let sketch = expect_sketch(sketch_object);
9425 let line1_id = *sketch.segments.get(2).unwrap();
9426
9427 let (src_delta, scene_delta) = frontend
9428 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9429 .await
9430 .unwrap();
9431 assert_eq!(
9432 src_delta.text.as_str(),
9433 "\
9434sketch(on = XY) {
9435 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9436}
9437"
9438 );
9439 assert_eq!(
9440 scene_delta.new_graph.objects.len(),
9441 5,
9442 "{:#?}",
9443 scene_delta.new_graph.objects
9444 );
9445
9446 mock_ctx.close().await;
9447 }
9448
9449 #[tokio::test(flavor = "multi_thread")]
9450 async fn test_delete_line_cascades_to_midpoint_constraint() {
9451 let initial_source = "\
9452sketch(on = XY) {
9453 point1 = point(at = [var 1, var 2])
9454 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9455 midpoint(line1, point = point1)
9456}
9457";
9458
9459 let program = Program::parse(initial_source).unwrap().0.unwrap();
9460
9461 let mut frontend = FrontendState::new();
9462
9463 let mock_ctx = ExecutorContext::new_mock(None).await;
9464 let version = Version(0);
9465
9466 frontend.program = program.clone();
9467 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9468 frontend.update_state_after_exec(outcome, true);
9469 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9470 let sketch_id = sketch_object.id;
9471 let sketch = expect_sketch(sketch_object);
9472 let line1_id = *sketch.segments.get(3).unwrap();
9473
9474 let (src_delta, scene_delta) = frontend
9475 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9476 .await
9477 .unwrap();
9478 assert_eq!(
9479 src_delta.text.as_str(),
9480 "\
9481sketch(on = XY) {
9482 point1 = point(at = [var 1, var 2])
9483}
9484"
9485 );
9486 assert_eq!(
9487 scene_delta.new_graph.objects.len(),
9488 3,
9489 "{:#?}",
9490 scene_delta.new_graph.objects
9491 );
9492
9493 mock_ctx.close().await;
9494 }
9495
9496 #[tokio::test(flavor = "multi_thread")]
9497 async fn test_delete_point_preserves_multiline_coincident_constraint() {
9498 let initial_source = "\
9499sketch(on = XY) {
9500 point1 = point(at = [var 1, var 2])
9501 point2 = point(at = [var 3, var 4])
9502 point3 = point(at = [var 5, var 6])
9503 coincident([point1, point2, point3])
9504}
9505";
9506
9507 let program = Program::parse(initial_source).unwrap().0.unwrap();
9508
9509 let mut frontend = FrontendState::new();
9510
9511 let mock_ctx = ExecutorContext::new_mock(None).await;
9512 let version = Version(0);
9513
9514 frontend.program = program.clone();
9515 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9516 frontend.update_state_after_exec(outcome, true);
9517 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9518 let sketch_id = sketch_object.id;
9519 let sketch = expect_sketch(sketch_object);
9520 let point3_id = *sketch.segments.get(2).unwrap();
9521
9522 let (src_delta, scene_delta) = frontend
9523 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
9524 .await
9525 .unwrap();
9526 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
9527 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
9528 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
9529 assert!(
9530 src_delta.text.contains("coincident([point1, point2])"),
9531 "{}",
9532 src_delta.text
9533 );
9534
9535 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9536 let sketch = expect_sketch(sketch_object);
9537 assert_eq!(sketch.segments.len(), 2);
9538 assert_eq!(sketch.constraints.len(), 1);
9539
9540 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9541 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9542 panic!("Expected constraint object");
9543 };
9544 let Constraint::Coincident(coincident) = constraint else {
9545 panic!("Expected coincident constraint");
9546 };
9547 assert_eq!(
9548 coincident.segments,
9549 sketch
9550 .segments
9551 .iter()
9552 .copied()
9553 .map(Into::into)
9554 .collect::<Vec<ConstraintSegment>>()
9555 );
9556
9557 mock_ctx.close().await;
9558 }
9559
9560 #[tokio::test(flavor = "multi_thread")]
9561 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
9562 let initial_source = "\
9563sketch(on = XY) {
9564 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9565 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9566 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9567 equalLength([line1, line2, line3])
9568}
9569";
9570
9571 let program = Program::parse(initial_source).unwrap().0.unwrap();
9572
9573 let mut frontend = FrontendState::new();
9574
9575 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9576 let mock_ctx = ExecutorContext::new_mock(None).await;
9577 let version = Version(0);
9578
9579 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9580 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9581 let sketch_id = sketch_object.id;
9582 let sketch = expect_sketch(sketch_object);
9583 let line3_id = *sketch.segments.get(8).unwrap();
9584
9585 let (src_delta, scene_delta) = frontend
9586 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9587 .await
9588 .unwrap();
9589 assert_eq!(
9590 src_delta.text.as_str(),
9591 "\
9592sketch(on = XY) {
9593 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9594 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9595 equalLength([line1, line2])
9596}
9597"
9598 );
9599
9600 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9601 let sketch = expect_sketch(sketch_object);
9602 assert_eq!(sketch.constraints.len(), 1);
9603
9604 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9605 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9606 panic!("Expected constraint object");
9607 };
9608 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
9609 panic!("Expected lines equal length constraint");
9610 };
9611 assert_eq!(lines_equal_length.lines.len(), 2);
9612
9613 ctx.close().await;
9614 mock_ctx.close().await;
9615 }
9616
9617 #[tokio::test(flavor = "multi_thread")]
9618 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
9619 let initial_source = "\
9620sketch(on = XY) {
9621 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9622 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9623 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9624 horizontal([line1.end, line2.start, line3.start])
9625}
9626";
9627
9628 let program = Program::parse(initial_source).unwrap().0.unwrap();
9629
9630 let mut frontend = FrontendState::new();
9631
9632 let mock_ctx = ExecutorContext::new_mock(None).await;
9633 let version = Version(0);
9634
9635 frontend.program = program.clone();
9636 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9637 frontend.update_state_after_exec(outcome, true);
9638 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9639 let sketch_id = sketch_object.id;
9640 let sketch = expect_sketch(sketch_object);
9641 let line1_id = *sketch.segments.get(2).unwrap();
9642
9643 let (src_delta, scene_delta) = frontend
9644 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9645 .await
9646 .unwrap();
9647 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9648 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9649 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9650 assert!(
9651 src_delta.text.contains("horizontal([line2.start, line3.start])"),
9652 "{}",
9653 src_delta.text
9654 );
9655
9656 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9657 let sketch = expect_sketch(sketch_object);
9658 assert_eq!(sketch.constraints.len(), 1);
9659
9660 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9661 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9662 panic!("Expected constraint object");
9663 };
9664 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
9665 panic!("Expected horizontal points constraint");
9666 };
9667 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9668 assert_eq!(*points, remaining_points);
9669
9670 mock_ctx.close().await;
9671 }
9672
9673 #[tokio::test(flavor = "multi_thread")]
9674 async fn test_delete_line_preserves_multiline_vertical_constraint() {
9675 let initial_source = "\
9676sketch(on = XY) {
9677 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9678 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9679 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9680 vertical([line1.end, line2.start, line3.start])
9681}
9682";
9683
9684 let program = Program::parse(initial_source).unwrap().0.unwrap();
9685
9686 let mut frontend = FrontendState::new();
9687
9688 let mock_ctx = ExecutorContext::new_mock(None).await;
9689 let version = Version(0);
9690
9691 frontend.program = program.clone();
9692 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9693 frontend.update_state_after_exec(outcome, true);
9694 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9695 let sketch_id = sketch_object.id;
9696 let sketch = expect_sketch(sketch_object);
9697 let line1_id = *sketch.segments.get(2).unwrap();
9698
9699 let (src_delta, scene_delta) = frontend
9700 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9701 .await
9702 .unwrap();
9703 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9704 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9705 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9706 assert!(
9707 src_delta.text.contains("vertical([line2.start, line3.start])"),
9708 "{}",
9709 src_delta.text
9710 );
9711
9712 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9713 let sketch = expect_sketch(sketch_object);
9714 assert_eq!(sketch.constraints.len(), 1);
9715
9716 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9717 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9718 panic!("Expected constraint object");
9719 };
9720 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
9721 panic!("Expected vertical points constraint");
9722 };
9723 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9724 assert_eq!(*points, remaining_points);
9725
9726 mock_ctx.close().await;
9727 }
9728
9729 #[tokio::test(flavor = "multi_thread")]
9730 async fn test_delete_line_preserves_multiline_coincident_constraint() {
9731 let initial_source = "\
9732sketch(on = XY) {
9733 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9734 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9735 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9736 coincident([line1.end, line2.start, line3.start])
9737}
9738";
9739
9740 let program = Program::parse(initial_source).unwrap().0.unwrap();
9741
9742 let mut frontend = FrontendState::new();
9743
9744 let mock_ctx = ExecutorContext::new_mock(None).await;
9745 let version = Version(0);
9746
9747 frontend.program = program.clone();
9748 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9749 frontend.update_state_after_exec(outcome, true);
9750 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9751 let sketch_id = sketch_object.id;
9752 let sketch = expect_sketch(sketch_object);
9753 let line1_id = *sketch.segments.get(2).unwrap();
9754
9755 let (src_delta, scene_delta) = frontend
9756 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9757 .await
9758 .unwrap();
9759 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9760 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9761 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9762 assert!(
9763 src_delta.text.contains("coincident([line2.start, line3.start])"),
9764 "{}",
9765 src_delta.text
9766 );
9767
9768 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9769 let sketch = expect_sketch(sketch_object);
9770 assert_eq!(sketch.constraints.len(), 1);
9771
9772 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9773 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9774 panic!("Expected constraint object");
9775 };
9776 let Constraint::Coincident(coincident) = constraint else {
9777 panic!("Expected coincident constraint");
9778 };
9779 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9780 assert_eq!(coincident.segments, remaining_segments);
9781
9782 mock_ctx.close().await;
9783 }
9784
9785 #[tokio::test(flavor = "multi_thread")]
9786 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
9787 let initial_source = "\
9788sketch(on = XY) {
9789 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9790 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9791 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9792 equalLength([line1, line2, line3])
9793}
9794";
9795
9796 let program = Program::parse(initial_source).unwrap().0.unwrap();
9797
9798 let mut frontend = FrontendState::new();
9799
9800 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9801 let mock_ctx = ExecutorContext::new_mock(None).await;
9802 let version = Version(0);
9803
9804 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9805 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9806 let sketch_id = sketch_object.id;
9807 let sketch = expect_sketch(sketch_object);
9808 let line2_id = *sketch.segments.get(5).unwrap();
9809 let line3_id = *sketch.segments.get(8).unwrap();
9810
9811 let (src_delta, scene_delta) = frontend
9812 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
9813 .await
9814 .unwrap();
9815 assert_eq!(
9816 src_delta.text.as_str(),
9817 "\
9818sketch(on = XY) {
9819 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9820}
9821"
9822 );
9823
9824 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9825 let sketch = expect_sketch(sketch_object);
9826 assert!(sketch.constraints.is_empty());
9827
9828 ctx.close().await;
9829 mock_ctx.close().await;
9830 }
9831
9832 #[tokio::test(flavor = "multi_thread")]
9833 async fn test_delete_line_preserves_multiline_parallel_constraint() {
9834 let initial_source = "\
9835sketch(on = XY) {
9836 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9837 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9838 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9839 parallel([line1, line2, line3])
9840}
9841";
9842
9843 let program = Program::parse(initial_source).unwrap().0.unwrap();
9844
9845 let mut frontend = FrontendState::new();
9846
9847 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9848 let mock_ctx = ExecutorContext::new_mock(None).await;
9849 let version = Version(0);
9850
9851 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9852 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9853 let sketch_id = sketch_object.id;
9854 let sketch = expect_sketch(sketch_object);
9855 let line3_id = *sketch.segments.get(8).unwrap();
9856
9857 let (src_delta, scene_delta) = frontend
9858 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9859 .await
9860 .unwrap();
9861 assert_eq!(
9862 src_delta.text.as_str(),
9863 "\
9864sketch(on = XY) {
9865 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9866 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9867 parallel([line1, line2])
9868}
9869"
9870 );
9871
9872 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9873 let sketch = expect_sketch(sketch_object);
9874 assert_eq!(sketch.constraints.len(), 1);
9875
9876 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9877 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9878 panic!("Expected constraint object");
9879 };
9880 let Constraint::Parallel(parallel) = constraint else {
9881 panic!("Expected parallel constraint");
9882 };
9883 assert_eq!(parallel.lines.len(), 2);
9884
9885 ctx.close().await;
9886 mock_ctx.close().await;
9887 }
9888
9889 #[tokio::test(flavor = "multi_thread")]
9890 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
9891 let initial_source = "\
9892sketch(on = XY) {
9893 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9894 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9895 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9896 parallel([line1, line2, line3])
9897}
9898";
9899
9900 let program = Program::parse(initial_source).unwrap().0.unwrap();
9901
9902 let mut frontend = FrontendState::new();
9903
9904 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9905 let mock_ctx = ExecutorContext::new_mock(None).await;
9906 let version = Version(0);
9907
9908 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9909 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9910 let sketch_id = sketch_object.id;
9911 let sketch = expect_sketch(sketch_object);
9912 let line2_id = *sketch.segments.get(5).unwrap();
9913 let line3_id = *sketch.segments.get(8).unwrap();
9914
9915 let (src_delta, scene_delta) = frontend
9916 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
9917 .await
9918 .unwrap();
9919 assert_eq!(
9920 src_delta.text.as_str(),
9921 "\
9922sketch(on = XY) {
9923 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9924}
9925"
9926 );
9927
9928 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9929 let sketch = expect_sketch(sketch_object);
9930 assert!(sketch.constraints.is_empty());
9931
9932 ctx.close().await;
9933 mock_ctx.close().await;
9934 }
9935
9936 #[tokio::test(flavor = "multi_thread")]
9937 async fn test_delete_line_line_coincident_constraint() {
9938 let initial_source = "\
9939sketch(on = XY) {
9940 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9941 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9942 coincident([line1, line2])
9943}
9944";
9945
9946 let program = Program::parse(initial_source).unwrap().0.unwrap();
9947
9948 let mut frontend = FrontendState::new();
9949
9950 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9951 let mock_ctx = ExecutorContext::new_mock(None).await;
9952 let version = Version(0);
9953
9954 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9955 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9956 let sketch_id = sketch_object.id;
9957 let sketch = expect_sketch(sketch_object);
9958
9959 let coincident_id = *sketch.constraints.first().unwrap();
9960
9961 let (src_delta, scene_delta) = frontend
9962 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9963 .await
9964 .unwrap();
9965 assert_eq!(
9966 src_delta.text.as_str(),
9967 "\
9968sketch(on = XY) {
9969 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9970 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9971}
9972"
9973 );
9974 assert_eq!(scene_delta.new_objects, vec![]);
9975 assert_eq!(scene_delta.new_graph.objects.len(), 8);
9976
9977 ctx.close().await;
9978 mock_ctx.close().await;
9979 }
9980
9981 #[tokio::test(flavor = "multi_thread")]
9982 async fn test_two_points_coincident() {
9983 let initial_source = "\
9984sketch(on = XY) {
9985 point1 = point(at = [var 1, var 2])
9986 point(at = [3, 4])
9987}
9988";
9989
9990 let program = Program::parse(initial_source).unwrap().0.unwrap();
9991
9992 let mut frontend = FrontendState::new();
9993
9994 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9995 let mock_ctx = ExecutorContext::new_mock(None).await;
9996 let version = Version(0);
9997
9998 frontend.hack_set_program(&ctx, program).await.unwrap();
9999 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10000 let sketch_id = sketch_object.id;
10001 let sketch = expect_sketch(sketch_object);
10002 let point0_id = *sketch.segments.first().unwrap();
10003 let point1_id = *sketch.segments.get(1).unwrap();
10004
10005 let constraint = Constraint::Coincident(Coincident {
10006 segments: vec![point0_id.into(), point1_id.into()],
10007 });
10008 let (src_delta, scene_delta) = frontend
10009 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10010 .await
10011 .unwrap();
10012 assert_eq!(
10013 src_delta.text.as_str(),
10014 "\
10015sketch(on = XY) {
10016 point1 = point(at = [var 3, var 4])
10017 point2 = point(at = [3, 4])
10018 coincident([point1, point2])
10019}
10020"
10021 );
10022 assert_eq!(
10023 scene_delta.new_graph.objects.len(),
10024 5,
10025 "{:#?}",
10026 scene_delta.new_graph.objects
10027 );
10028
10029 ctx.close().await;
10030 mock_ctx.close().await;
10031 }
10032
10033 #[tokio::test(flavor = "multi_thread")]
10034 async fn test_three_points_coincident() {
10035 let initial_source = "\
10036sketch(on = XY) {
10037 point1 = point(at = [var 1, var 2])
10038 point(at = [var 3, var 4])
10039 point(at = [var 5, var 6])
10040}
10041";
10042
10043 let program = Program::parse(initial_source).unwrap().0.unwrap();
10044
10045 let mut frontend = FrontendState::new();
10046
10047 let mock_ctx = ExecutorContext::new_mock(None).await;
10048 let version = Version(0);
10049
10050 frontend.program = program.clone();
10051 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10052 frontend.update_state_after_exec(outcome, true);
10053 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10054 let sketch_id = sketch_object.id;
10055 let sketch = expect_sketch(sketch_object);
10056 let segments = sketch
10057 .segments
10058 .iter()
10059 .take(3)
10060 .copied()
10061 .map(Into::into)
10062 .collect::<Vec<ConstraintSegment>>();
10063
10064 let constraint = Constraint::Coincident(Coincident {
10065 segments: segments.clone(),
10066 });
10067 let (src_delta, scene_delta) = frontend
10068 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10069 .await
10070 .unwrap();
10071 assert_eq!(
10072 src_delta.text.as_str(),
10073 "\
10074sketch(on = XY) {
10075 point1 = point(at = [var 3, var 4])
10076 point2 = point(at = [var 3, var 4])
10077 point3 = point(at = [var 3, var 4])
10078 coincident([point1, point2, point3])
10079}
10080"
10081 );
10082
10083 let constraint_object = scene_delta
10084 .new_graph
10085 .objects
10086 .iter()
10087 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10088 .unwrap();
10089
10090 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10091 panic!("expected a constraint object");
10092 };
10093
10094 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10095
10096 mock_ctx.close().await;
10097 }
10098
10099 #[tokio::test(flavor = "multi_thread")]
10100 async fn test_source_with_three_point_coincident_tracks_all_segments() {
10101 let initial_source = "\
10102sketch(on = XY) {
10103 point1 = point(at = [var 1, var 2])
10104 point2 = point(at = [var 3, var 4])
10105 point3 = point(at = [var 5, var 6])
10106 coincident([point1, point2, point3])
10107}
10108";
10109
10110 let program = Program::parse(initial_source).unwrap().0.unwrap();
10111
10112 let mut frontend = FrontendState::new();
10113
10114 let ctx = ExecutorContext::new_mock(None).await;
10115 frontend.program = program.clone();
10116 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10117 frontend.update_state_after_exec(outcome, true);
10118
10119 let constraint_object = frontend
10120 .scene_graph
10121 .objects
10122 .iter()
10123 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10124 .unwrap();
10125 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10126 panic!("expected a constraint object");
10127 };
10128
10129 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10130 let sketch = expect_sketch(sketch_object);
10131 let expected_segments = sketch
10132 .segments
10133 .iter()
10134 .take(3)
10135 .copied()
10136 .map(Into::into)
10137 .collect::<Vec<ConstraintSegment>>();
10138
10139 assert_eq!(
10140 constraint,
10141 &Constraint::Coincident(Coincident {
10142 segments: expected_segments,
10143 })
10144 );
10145
10146 ctx.close().await;
10147 }
10148
10149 #[tokio::test(flavor = "multi_thread")]
10150 async fn test_point_origin_coincident_preserves_order() {
10151 let initial_source = "\
10152sketch(on = XY) {
10153 point(at = [var 1, var 2])
10154}
10155";
10156
10157 for (origin_first, expected_source) in [
10158 (
10159 true,
10160 "\
10161sketch(on = XY) {
10162 point1 = point(at = [var 0, var 0])
10163 coincident([ORIGIN, point1])
10164}
10165",
10166 ),
10167 (
10168 false,
10169 "\
10170sketch(on = XY) {
10171 point1 = point(at = [var 0, var 0])
10172 coincident([point1, ORIGIN])
10173}
10174",
10175 ),
10176 ] {
10177 let program = Program::parse(initial_source).unwrap().0.unwrap();
10178
10179 let mut frontend = FrontendState::new();
10180
10181 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10182 let mock_ctx = ExecutorContext::new_mock(None).await;
10183 let version = Version(0);
10184
10185 frontend.hack_set_program(&ctx, program).await.unwrap();
10186 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10187 let sketch_id = sketch_object.id;
10188 let sketch = expect_sketch(sketch_object);
10189 let point_id = *sketch.segments.first().unwrap();
10190
10191 let segments = if origin_first {
10192 vec![ConstraintSegment::ORIGIN, point_id.into()]
10193 } else {
10194 vec![point_id.into(), ConstraintSegment::ORIGIN]
10195 };
10196 let constraint = Constraint::Coincident(Coincident {
10197 segments: segments.clone(),
10198 });
10199 let (src_delta, scene_delta) = frontend
10200 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10201 .await
10202 .unwrap();
10203 assert_eq!(src_delta.text.as_str(), expected_source);
10204
10205 let constraint_object = scene_delta
10206 .new_graph
10207 .objects
10208 .iter()
10209 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10210 .unwrap();
10211
10212 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10213 panic!("expected a constraint object");
10214 };
10215
10216 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10217
10218 ctx.close().await;
10219 mock_ctx.close().await;
10220 }
10221 }
10222
10223 #[tokio::test(flavor = "multi_thread")]
10224 async fn test_coincident_of_line_end_points() {
10225 let initial_source = "\
10226sketch(on = XY) {
10227 line(start = [var 1, var 2], end = [var 3, var 4])
10228 line(start = [var 5, var 6], end = [var 7, var 8])
10229}
10230";
10231
10232 let program = Program::parse(initial_source).unwrap().0.unwrap();
10233
10234 let mut frontend = FrontendState::new();
10235
10236 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10237 let mock_ctx = ExecutorContext::new_mock(None).await;
10238 let version = Version(0);
10239
10240 frontend.hack_set_program(&ctx, program).await.unwrap();
10241 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10242 let sketch_id = sketch_object.id;
10243 let sketch = expect_sketch(sketch_object);
10244 let point0_id = *sketch.segments.get(1).unwrap();
10245 let point1_id = *sketch.segments.get(3).unwrap();
10246
10247 let constraint = Constraint::Coincident(Coincident {
10248 segments: vec![point0_id.into(), point1_id.into()],
10249 });
10250 let (src_delta, scene_delta) = frontend
10251 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10252 .await
10253 .unwrap();
10254 assert_eq!(
10255 src_delta.text.as_str(),
10256 "\
10257sketch(on = XY) {
10258 line1 = line(start = [var 1, var 2], end = [var 4, var 5])
10259 line2 = line(start = [var 4, var 5], end = [var 7, var 8])
10260 coincident([line1.end, line2.start])
10261}
10262"
10263 );
10264 assert_eq!(
10265 scene_delta.new_graph.objects.len(),
10266 9,
10267 "{:#?}",
10268 scene_delta.new_graph.objects
10269 );
10270
10271 ctx.close().await;
10272 mock_ctx.close().await;
10273 }
10274
10275 #[tokio::test(flavor = "multi_thread")]
10276 async fn test_coincident_of_line_point_and_circle_segment() {
10277 let initial_source = "\
10278sketch(on = XY) {
10279 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10280 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10281}
10282";
10283 let program = Program::parse(initial_source).unwrap().0.unwrap();
10284 let mut frontend = FrontendState::new();
10285
10286 let mock_ctx = ExecutorContext::new_mock(None).await;
10287 let version = Version(0);
10288
10289 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10290 frontend.program = program;
10291 frontend.update_state_after_exec(outcome, true);
10292 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10293 let sketch_id = sketch_object.id;
10294 let sketch = expect_sketch(sketch_object);
10295
10296 let circle_id = sketch
10297 .segments
10298 .iter()
10299 .copied()
10300 .find(|seg_id| {
10301 matches!(
10302 &frontend.scene_graph.objects[seg_id.0].kind,
10303 ObjectKind::Segment {
10304 segment: Segment::Circle(_)
10305 }
10306 )
10307 })
10308 .expect("Expected a circle segment in sketch");
10309 let line_id = frontend
10310 .scene_graph
10311 .objects
10312 .iter()
10313 .find_map(|obj| match &obj.kind {
10314 ObjectKind::Segment {
10315 segment: Segment::Line(line),
10316 } if line.owner.is_none() => Some(obj.id),
10317 _ => None,
10318 })
10319 .expect("Expected a standalone line segment in scene graph");
10320
10321 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10322 ObjectKind::Segment {
10323 segment: Segment::Line(line),
10324 } => line.start,
10325 _ => panic!("Expected line segment object"),
10326 };
10327
10328 let constraint = Constraint::Coincident(Coincident {
10329 segments: vec![line_start_point_id.into(), circle_id.into()],
10330 });
10331 let (src_delta, _scene_delta) = frontend
10332 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10333 .await
10334 .unwrap();
10335 assert_eq!(
10336 src_delta.text.as_str(),
10337 "\
10338sketch(on = XY) {
10339 circle1 = circle(start = [var 7.02mm, var 0mm], center = [var -0.01mm, var 0.22mm])
10340 line1 = line(start = [var 7mm, var 0.78mm], end = [var 10mm, var 2mm])
10341 coincident([line1.start, circle1])
10342}
10343"
10344 );
10345
10346 mock_ctx.close().await;
10347 }
10348
10349 #[tokio::test(flavor = "multi_thread")]
10350 async fn test_invalid_coincident_arc_and_line_preserves_state() {
10351 let program = Program::empty();
10359
10360 let mut frontend = FrontendState::new();
10361 frontend.program = program;
10362
10363 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10364 let mock_ctx = ExecutorContext::new_mock(None).await;
10365 let version = Version(0);
10366
10367 let sketch_args = SketchCtor {
10368 on: Plane::Default(PlaneName::Xy),
10369 };
10370 let (_src_delta, _scene_delta, sketch_id) = frontend
10371 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10372 .await
10373 .unwrap();
10374
10375 let arc_ctor = ArcCtor {
10377 start: Point2d {
10378 x: Expr::Var(Number {
10379 value: 0.0,
10380 units: NumericSuffix::Mm,
10381 }),
10382 y: Expr::Var(Number {
10383 value: 0.0,
10384 units: NumericSuffix::Mm,
10385 }),
10386 },
10387 end: Point2d {
10388 x: Expr::Var(Number {
10389 value: 10.0,
10390 units: NumericSuffix::Mm,
10391 }),
10392 y: Expr::Var(Number {
10393 value: 10.0,
10394 units: NumericSuffix::Mm,
10395 }),
10396 },
10397 center: Point2d {
10398 x: Expr::Var(Number {
10399 value: 10.0,
10400 units: NumericSuffix::Mm,
10401 }),
10402 y: Expr::Var(Number {
10403 value: 0.0,
10404 units: NumericSuffix::Mm,
10405 }),
10406 },
10407 construction: None,
10408 };
10409 let (_src_delta, scene_delta) = frontend
10410 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10411 .await
10412 .unwrap();
10413 let arc_id = *scene_delta.new_objects.last().unwrap();
10415
10416 let line_ctor = LineCtor {
10418 start: Point2d {
10419 x: Expr::Var(Number {
10420 value: 20.0,
10421 units: NumericSuffix::Mm,
10422 }),
10423 y: Expr::Var(Number {
10424 value: 0.0,
10425 units: NumericSuffix::Mm,
10426 }),
10427 },
10428 end: Point2d {
10429 x: Expr::Var(Number {
10430 value: 30.0,
10431 units: NumericSuffix::Mm,
10432 }),
10433 y: Expr::Var(Number {
10434 value: 10.0,
10435 units: NumericSuffix::Mm,
10436 }),
10437 },
10438 construction: None,
10439 };
10440 let (_src_delta, scene_delta) = frontend
10441 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10442 .await
10443 .unwrap();
10444 let line_id = *scene_delta.new_objects.last().unwrap();
10446
10447 let constraint = Constraint::Coincident(Coincident {
10450 segments: vec![arc_id.into(), line_id.into()],
10451 });
10452 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10453
10454 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10456
10457 let sketch_object_after =
10460 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10461 let sketch_after = expect_sketch(sketch_object_after);
10462
10463 assert!(
10465 sketch_after.segments.contains(&arc_id),
10466 "Arc segment should still exist after failed constraint"
10467 );
10468 assert!(
10469 sketch_after.segments.contains(&line_id),
10470 "Line segment should still exist after failed constraint"
10471 );
10472
10473 let arc_obj = frontend
10475 .scene_graph
10476 .objects
10477 .get(arc_id.0)
10478 .expect("Arc object should still be accessible");
10479 let line_obj = frontend
10480 .scene_graph
10481 .objects
10482 .get(line_id.0)
10483 .expect("Line object should still be accessible");
10484
10485 match &arc_obj.kind {
10488 ObjectKind::Segment {
10489 segment: Segment::Arc(_),
10490 } => {}
10491 _ => panic!("Arc object should still be an arc segment"),
10492 }
10493 match &line_obj.kind {
10494 ObjectKind::Segment {
10495 segment: Segment::Line(_),
10496 } => {}
10497 _ => panic!("Line object should still be a line segment"),
10498 }
10499
10500 ctx.close().await;
10501 mock_ctx.close().await;
10502 }
10503
10504 #[tokio::test(flavor = "multi_thread")]
10505 async fn test_distance_two_points() {
10506 let initial_source = "\
10507sketch(on = XY) {
10508 point(at = [var 1, var 2])
10509 point(at = [var 3, var 4])
10510}
10511";
10512
10513 let program = Program::parse(initial_source).unwrap().0.unwrap();
10514
10515 let mut frontend = FrontendState::new();
10516
10517 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10518 let mock_ctx = ExecutorContext::new_mock(None).await;
10519 let version = Version(0);
10520
10521 frontend.hack_set_program(&ctx, program).await.unwrap();
10522 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10523 let sketch_id = sketch_object.id;
10524 let sketch = expect_sketch(sketch_object);
10525 let point0_id = *sketch.segments.first().unwrap();
10526 let point1_id = *sketch.segments.get(1).unwrap();
10527
10528 let constraint = Constraint::Distance(Distance {
10529 points: vec![point0_id.into(), point1_id.into()],
10530 distance: Number {
10531 value: 2.0,
10532 units: NumericSuffix::Mm,
10533 },
10534 label_position: None,
10535 source: Default::default(),
10536 });
10537 let (src_delta, scene_delta) = frontend
10538 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10539 .await
10540 .unwrap();
10541 assert_eq!(
10542 src_delta.text.as_str(),
10543 "\
10545sketch(on = XY) {
10546 point1 = point(at = [var 1.29, var 2.29])
10547 point2 = point(at = [var 2.71, var 3.71])
10548 distance([point1, point2]) == 2mm
10549}
10550"
10551 );
10552 assert_eq!(
10553 scene_delta.new_graph.objects.len(),
10554 5,
10555 "{:#?}",
10556 scene_delta.new_graph.objects
10557 );
10558
10559 ctx.close().await;
10560 mock_ctx.close().await;
10561 }
10562
10563 #[tokio::test(flavor = "multi_thread")]
10564 async fn test_distance_two_points_with_label() {
10565 let initial_source = "\
10566sketch(on = XY) {
10567 point(at = [var 1, var 2])
10568 point(at = [var 3, var 4])
10569}
10570";
10571
10572 let program = Program::parse(initial_source).unwrap().0.unwrap();
10573
10574 let mut frontend = FrontendState::new();
10575
10576 let mock_ctx = ExecutorContext::new_mock(None).await;
10577 let version = Version(0);
10578
10579 frontend.program = program.clone();
10580 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10581 frontend.update_state_after_exec(outcome, true);
10582 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10583 let sketch_id = sketch_object.id;
10584 let sketch = expect_sketch(sketch_object);
10585 let point0_id = *sketch.segments.first().unwrap();
10586 let point1_id = *sketch.segments.get(1).unwrap();
10587
10588 let label_position = Point2d {
10589 x: Number {
10590 value: 10.0,
10591 units: NumericSuffix::Mm,
10592 },
10593 y: Number {
10594 value: 11.0,
10595 units: NumericSuffix::Mm,
10596 },
10597 };
10598 let constraint = Constraint::Distance(Distance {
10599 points: vec![point0_id.into(), point1_id.into()],
10600 distance: Number {
10601 value: 2.0,
10602 units: NumericSuffix::Mm,
10603 },
10604 label_position: Some(label_position.clone()),
10605 source: Default::default(),
10606 });
10607 let (src_delta, scene_delta) = frontend
10608 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10609 .await
10610 .unwrap();
10611 assert_eq!(
10612 src_delta.text.as_str(),
10613 "\
10614sketch(on = XY) {
10615 point1 = point(at = [var 1.29, var 2.29])
10616 point2 = point(at = [var 2.71, var 3.71])
10617 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10618}
10619"
10620 );
10621
10622 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10623 let sketch = expect_sketch(sketch_object);
10624 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10625 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10626 panic!("Expected constraint object");
10627 };
10628 let Constraint::Distance(distance) = constraint else {
10629 panic!("Expected distance constraint");
10630 };
10631 assert_eq!(distance.label_position, Some(label_position));
10632
10633 mock_ctx.close().await;
10634 }
10635
10636 #[tokio::test(flavor = "multi_thread")]
10637 async fn test_edit_distance_constraint_label_position() {
10638 let initial_source = "\
10639sketch(on = XY) {
10640 point(at = [var 1, var 2])
10641 point(at = [var 3, var 2])
10642}
10643";
10644
10645 let program = Program::parse(initial_source).unwrap().0.unwrap();
10646
10647 let mut frontend = FrontendState::new();
10648
10649 let mock_ctx = ExecutorContext::new_mock(None).await;
10650 let version = Version(0);
10651
10652 frontend.program = program.clone();
10653 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10654 frontend.update_state_after_exec(outcome, true);
10655 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10656 let sketch_id = sketch_object.id;
10657 let sketch = expect_sketch(sketch_object);
10658 let point0_id = *sketch.segments.first().unwrap();
10659 let point1_id = *sketch.segments.get(1).unwrap();
10660
10661 let constraint = Constraint::Distance(Distance {
10662 points: vec![point0_id.into(), point1_id.into()],
10663 distance: Number {
10664 value: 2.0,
10665 units: NumericSuffix::Mm,
10666 },
10667 label_position: None,
10668 source: Default::default(),
10669 });
10670 let (_, scene_delta) = frontend
10671 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10672 .await
10673 .unwrap();
10674 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10675 let sketch = expect_sketch(sketch_object);
10676 let constraint_id = sketch.constraints[0];
10677 let label_position = Point2d {
10678 x: Number {
10679 value: 10.0,
10680 units: NumericSuffix::Mm,
10681 },
10682 y: Number {
10683 value: 11.0,
10684 units: NumericSuffix::Mm,
10685 },
10686 };
10687
10688 let (src_delta, scene_delta) = frontend
10689 .edit_distance_constraint_label_position(
10690 &mock_ctx,
10691 version,
10692 sketch_id,
10693 constraint_id,
10694 label_position.clone(),
10695 vec![],
10696 )
10697 .await
10698 .unwrap();
10699 assert_eq!(
10700 src_delta.text.as_str(),
10701 "\
10702sketch(on = XY) {
10703 point1 = point(at = [var 1, var 2])
10704 point2 = point(at = [var 3, var 2])
10705 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10706}
10707"
10708 );
10709
10710 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10711 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10712 panic!("Expected constraint object");
10713 };
10714 let Constraint::Distance(distance) = constraint else {
10715 panic!("Expected distance constraint");
10716 };
10717 assert_eq!(distance.label_position, Some(label_position));
10718
10719 mock_ctx.close().await;
10720 }
10721
10722 #[tokio::test(flavor = "multi_thread")]
10723 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
10724 let initial_source = "\
10725sketch(on = XY) {
10726 point1 = point(at = [var 0mm, var 0mm])
10727 point2 = point(at = [var 10mm, var 0mm])
10728 distance([point1, point2]) == 5mm
10729}
10730";
10731
10732 let program = Program::parse(initial_source).unwrap().0.unwrap();
10733 let mut frontend = FrontendState::new();
10734 let mock_ctx = ExecutorContext::new_mock(None).await;
10735 let version = Version(0);
10736
10737 frontend.program = program.clone();
10738 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10739 frontend.update_state_after_exec(outcome, true);
10740 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10741 let sketch_id = sketch_object.id;
10742 let sketch = expect_sketch(sketch_object);
10743 let point0_id = sketch.segments[0];
10744 let point1_id = sketch.segments[1];
10745 let constraint_id = sketch.constraints[0];
10746
10747 let edited_segments = vec![ExistingSegmentCtor {
10748 id: point0_id,
10749 ctor: SegmentCtor::Point(PointCtor {
10750 position: Point2d {
10751 x: Expr::Var(Number {
10752 value: 2.0,
10753 units: NumericSuffix::Mm,
10754 }),
10755 y: Expr::Var(Number {
10756 value: 1.0,
10757 units: NumericSuffix::Mm,
10758 }),
10759 },
10760 }),
10761 }];
10762 let (_, scene_delta) = frontend
10763 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
10764 .await
10765 .unwrap();
10766 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
10767 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
10768
10769 let label_position = Point2d {
10770 x: Number {
10771 value: 3.0,
10772 units: NumericSuffix::Mm,
10773 },
10774 y: Number {
10775 value: 4.0,
10776 units: NumericSuffix::Mm,
10777 },
10778 };
10779 let (_, scene_delta) = frontend
10780 .edit_distance_constraint_label_position(
10781 &mock_ctx,
10782 version,
10783 sketch_id,
10784 constraint_id,
10785 label_position,
10786 vec![point0_id],
10787 )
10788 .await
10789 .unwrap();
10790
10791 assert_point_position_close(
10792 point_position(&scene_delta.new_graph, point0_id),
10793 point0_after_segment_edit,
10794 );
10795 assert_point_position_close(
10796 point_position(&scene_delta.new_graph, point1_id),
10797 point1_after_segment_edit,
10798 );
10799
10800 mock_ctx.close().await;
10801 }
10802
10803 #[tokio::test(flavor = "multi_thread")]
10804 async fn test_distance_point_line() {
10805 let initial_source = "\
10806sketch(on = XY) {
10807 point(at = [var 0, var 5])
10808 line(start = [var 0, var 0], end = [var 10, var 0])
10809}
10810";
10811
10812 let program = Program::parse(initial_source).unwrap().0.unwrap();
10813
10814 let mut frontend = FrontendState::new();
10815
10816 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10817 let mock_ctx = ExecutorContext::new_mock(None).await;
10818 let version = Version(0);
10819
10820 frontend.hack_set_program(&ctx, program).await.unwrap();
10821 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10822 let sketch_id = sketch_object.id;
10823 let sketch = expect_sketch(sketch_object);
10824 let point_id = *sketch.segments.first().unwrap();
10825 let line_id = *sketch
10826 .segments
10827 .iter()
10828 .find(|segment_id| {
10829 matches!(
10830 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10831 Some(ObjectKind::Segment {
10832 segment: Segment::Line(_)
10833 })
10834 )
10835 })
10836 .unwrap();
10837
10838 let label_position = Point2d {
10839 x: Number {
10840 value: 10.0,
10841 units: NumericSuffix::Mm,
10842 },
10843 y: Number {
10844 value: 11.0,
10845 units: NumericSuffix::Mm,
10846 },
10847 };
10848 let constraint = Constraint::Distance(Distance {
10849 points: vec![point_id.into(), line_id.into()],
10850 distance: Number {
10851 value: 5.0,
10852 units: NumericSuffix::Mm,
10853 },
10854 label_position: Some(label_position.clone()),
10855 source: Default::default(),
10856 });
10857 let (src_delta, scene_delta) = frontend
10858 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10859 .await
10860 .unwrap();
10861 assert_eq!(
10862 src_delta.text.as_str(),
10863 "\
10864sketch(on = XY) {
10865 point1 = point(at = [var 0, var 5])
10866 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
10867 distance([point1, line1], labelPosition = [10mm, 11mm]) == 5mm
10868}
10869"
10870 );
10871 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10872 let sketch = expect_sketch(sketch_object);
10873 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10874 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10875 panic!("Expected constraint object");
10876 };
10877 let Constraint::Distance(distance) = constraint else {
10878 panic!("Expected distance constraint");
10879 };
10880 assert_eq!(distance.label_position, Some(label_position));
10881
10882 ctx.close().await;
10883 mock_ctx.close().await;
10884 }
10885
10886 #[tokio::test(flavor = "multi_thread")]
10887 async fn test_distance_point_arc() {
10888 let initial_source = "\
10889sketch(on = XY) {
10890 point(at = [var 0, var 8])
10891 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
10892}
10893";
10894
10895 let program = Program::parse(initial_source).unwrap().0.unwrap();
10896
10897 let mut frontend = FrontendState::new();
10898
10899 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10900 let mock_ctx = ExecutorContext::new_mock(None).await;
10901 let version = Version(0);
10902
10903 frontend.hack_set_program(&ctx, program).await.unwrap();
10904 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10905 let sketch_id = sketch_object.id;
10906 let sketch = expect_sketch(sketch_object);
10907 let point_id = *sketch.segments.first().unwrap();
10908 let arc_id = *sketch
10909 .segments
10910 .iter()
10911 .find(|segment_id| {
10912 matches!(
10913 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10914 Some(ObjectKind::Segment {
10915 segment: Segment::Arc(_)
10916 })
10917 )
10918 })
10919 .unwrap();
10920
10921 let constraint = Constraint::Distance(Distance {
10922 points: vec![point_id.into(), arc_id.into()],
10923 distance: Number {
10924 value: 3.0,
10925 units: NumericSuffix::Mm,
10926 },
10927 label_position: None,
10928 source: Default::default(),
10929 });
10930 let (src_delta, _scene_delta) = frontend
10931 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10932 .await
10933 .unwrap();
10934 assert_eq!(
10935 src_delta.text.as_str(),
10936 "\
10937sketch(on = XY) {
10938 point1 = point(at = [var 0, var 8])
10939 arc1 = arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
10940 distance([point1, arc1]) == 3mm
10941}
10942"
10943 );
10944
10945 ctx.close().await;
10946 mock_ctx.close().await;
10947 }
10948
10949 #[tokio::test(flavor = "multi_thread")]
10950 async fn test_distance_arc_origin() {
10951 let initial_source = "\
10952sketch001 = sketch(on = XY) {
10953 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
10954}
10955";
10956
10957 let program = Program::parse(initial_source).unwrap().0.unwrap();
10958
10959 let mut frontend = FrontendState::new();
10960
10961 let mock_ctx = ExecutorContext::new_mock(None).await;
10962 let version = Version(0);
10963
10964 frontend.program = program.clone();
10965 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10966 frontend.update_state_after_exec(outcome, true);
10967 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10968 let sketch_id = sketch_object.id;
10969 let sketch = expect_sketch(sketch_object);
10970 let arc_id = *sketch
10971 .segments
10972 .iter()
10973 .find(|segment_id| {
10974 matches!(
10975 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10976 Some(ObjectKind::Segment {
10977 segment: Segment::Arc(_)
10978 })
10979 )
10980 })
10981 .unwrap();
10982
10983 let constraint = Constraint::Distance(Distance {
10984 points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
10985 distance: Number {
10986 value: 3.0,
10987 units: NumericSuffix::Mm,
10988 },
10989 label_position: None,
10990 source: Default::default(),
10991 });
10992 let (src_delta, _scene_delta) = frontend
10993 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10994 .await
10995 .unwrap();
10996 assert_eq!(
10997 src_delta.text.as_str(),
10998 "\
10999sketch001 = sketch(on = XY) {
11000 arc1 = arc(start = [var -4.16mm, var -0.43mm], end = [var -3.53mm, var 3.28mm], center = [var -4.91mm, var 1.61mm])
11001 distance([arc1, ORIGIN]) == 3mm
11002}
11003"
11004 );
11005
11006 mock_ctx.close().await;
11007 }
11008
11009 #[tokio::test(flavor = "multi_thread")]
11010 async fn test_distance_line_origin() {
11011 let initial_source = "\
11012sketch(on = XY) {
11013 line(start = [var 5, var 0], end = [var 5, var 10])
11014}
11015";
11016
11017 let program = Program::parse(initial_source).unwrap().0.unwrap();
11018
11019 let mut frontend = FrontendState::new();
11020
11021 let mock_ctx = ExecutorContext::new_mock(None).await;
11022 let version = Version(0);
11023
11024 frontend.program = program.clone();
11025 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11026 frontend.update_state_after_exec(outcome, true);
11027 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11028 let sketch_id = sketch_object.id;
11029 let sketch = expect_sketch(sketch_object);
11030 let line_id = *sketch
11031 .segments
11032 .iter()
11033 .find(|segment_id| {
11034 matches!(
11035 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11036 Some(ObjectKind::Segment {
11037 segment: Segment::Line(_)
11038 })
11039 )
11040 })
11041 .unwrap();
11042
11043 let constraint = Constraint::Distance(Distance {
11044 points: vec![ConstraintSegment::ORIGIN, line_id.into()],
11045 distance: Number {
11046 value: 5.0,
11047 units: NumericSuffix::Mm,
11048 },
11049 label_position: None,
11050 source: Default::default(),
11051 });
11052 let (src_delta, _scene_delta) = frontend
11053 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11054 .await
11055 .unwrap();
11056 assert_eq!(
11057 src_delta.text.as_str(),
11058 "\
11059sketch(on = XY) {
11060 line1 = line(start = [var 5, var 0], end = [var 5, var 10])
11061 distance([ORIGIN, line1]) == 5mm
11062}
11063"
11064 );
11065
11066 mock_ctx.close().await;
11067 }
11068
11069 #[tokio::test(flavor = "multi_thread")]
11070 async fn test_distance_line_circle() {
11071 let initial_source = "\
11072sketch(on = XY) {
11073 line(start = [var -10, var 8], end = [var 10, var 8])
11074 circle(start = [var 5, var 0], center = [var 0, var 0])
11075}
11076";
11077
11078 let program = Program::parse(initial_source).unwrap().0.unwrap();
11079
11080 let mut frontend = FrontendState::new();
11081
11082 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11083 let mock_ctx = ExecutorContext::new_mock(None).await;
11084 let version = Version(0);
11085
11086 frontend.hack_set_program(&ctx, program).await.unwrap();
11087 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11088 let sketch_id = sketch_object.id;
11089 let sketch = expect_sketch(sketch_object);
11090 let line_id = *sketch
11091 .segments
11092 .iter()
11093 .find(|segment_id| {
11094 matches!(
11095 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11096 Some(ObjectKind::Segment {
11097 segment: Segment::Line(_)
11098 })
11099 )
11100 })
11101 .unwrap();
11102 let circle_id = *sketch
11103 .segments
11104 .iter()
11105 .find(|segment_id| {
11106 matches!(
11107 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11108 Some(ObjectKind::Segment {
11109 segment: Segment::Circle(_)
11110 })
11111 )
11112 })
11113 .unwrap();
11114
11115 let constraint = Constraint::Distance(Distance {
11116 points: vec![line_id.into(), circle_id.into()],
11117 distance: Number {
11118 value: 3.0,
11119 units: NumericSuffix::Mm,
11120 },
11121 label_position: None,
11122 source: Default::default(),
11123 });
11124 let (src_delta, _scene_delta) = frontend
11125 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11126 .await
11127 .unwrap();
11128 assert_eq!(
11129 src_delta.text.as_str(),
11130 "\
11131sketch(on = XY) {
11132 line1 = line(start = [var -10, var 8], end = [var 10, var 8])
11133 circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
11134 distance([line1, circle1]) == 3mm
11135}
11136"
11137 );
11138
11139 ctx.close().await;
11140 mock_ctx.close().await;
11141 }
11142
11143 #[tokio::test(flavor = "multi_thread")]
11144 async fn test_distance_circle_arc() {
11145 let initial_source = "\
11146sketch(on = XY) {
11147 circle(start = [var 5, var 0], center = [var 0, var 0])
11148 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11149}
11150";
11151
11152 let program = Program::parse(initial_source).unwrap().0.unwrap();
11153
11154 let mut frontend = FrontendState::new();
11155
11156 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11157 let mock_ctx = ExecutorContext::new_mock(None).await;
11158 let version = Version(0);
11159
11160 frontend.hack_set_program(&ctx, program).await.unwrap();
11161 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11162 let sketch_id = sketch_object.id;
11163 let sketch = expect_sketch(sketch_object);
11164 let circle_id = *sketch
11165 .segments
11166 .iter()
11167 .find(|segment_id| {
11168 matches!(
11169 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11170 Some(ObjectKind::Segment {
11171 segment: Segment::Circle(_)
11172 })
11173 )
11174 })
11175 .unwrap();
11176 let arc_id = *sketch
11177 .segments
11178 .iter()
11179 .find(|segment_id| {
11180 matches!(
11181 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11182 Some(ObjectKind::Segment {
11183 segment: Segment::Arc(_)
11184 })
11185 )
11186 })
11187 .unwrap();
11188
11189 let constraint = Constraint::Distance(Distance {
11190 points: vec![circle_id.into(), arc_id.into()],
11191 distance: Number {
11192 value: 3.0,
11193 units: NumericSuffix::Mm,
11194 },
11195 label_position: None,
11196 source: Default::default(),
11197 });
11198 let (src_delta, _scene_delta) = frontend
11199 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11200 .await
11201 .unwrap();
11202 assert_eq!(
11203 src_delta.text.as_str(),
11204 "\
11205sketch(on = XY) {
11206 circle1 = circle(start = [var 4.33, var 0], center = [var -0.34, var -0.09])
11207 arc1 = arc(start = [var 15.33, var -0.01], end = [var 10.01, var 4.33], center = [var 11.34, var 0.53])
11208 distance([circle1, arc1]) == 3mm
11209}
11210"
11211 );
11212
11213 ctx.close().await;
11214 mock_ctx.close().await;
11215 }
11216
11217 #[tokio::test(flavor = "multi_thread")]
11218 async fn test_distance_parallel_lines() {
11219 let initial_source = "\
11220sketch(on = XY) {
11221 line(start = [var 0, var 0], end = [var 10, var 0])
11222 line(start = [var 0, var 5], end = [var 10, var 5])
11223}
11224";
11225
11226 let program = Program::parse(initial_source).unwrap().0.unwrap();
11227
11228 let mut frontend = FrontendState::new();
11229
11230 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11231 let mock_ctx = ExecutorContext::new_mock(None).await;
11232 let version = Version(0);
11233
11234 frontend.hack_set_program(&ctx, program).await.unwrap();
11235 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11236 let sketch_id = sketch_object.id;
11237 let sketch = expect_sketch(sketch_object);
11238 let line_ids = sketch
11239 .segments
11240 .iter()
11241 .copied()
11242 .filter(|segment_id| {
11243 matches!(
11244 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11245 Some(ObjectKind::Segment {
11246 segment: Segment::Line(_)
11247 })
11248 )
11249 })
11250 .collect::<Vec<_>>();
11251
11252 let constraint = Constraint::Distance(Distance {
11253 points: vec![line_ids[0].into(), line_ids[1].into()],
11254 distance: Number {
11255 value: 5.0,
11256 units: NumericSuffix::Mm,
11257 },
11258 label_position: None,
11259 source: Default::default(),
11260 });
11261 let (src_delta, _scene_delta) = frontend
11262 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11263 .await
11264 .unwrap();
11265 assert_eq!(
11266 src_delta.text.as_str(),
11267 "\
11268sketch(on = XY) {
11269 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
11270 line2 = line(start = [var 0, var 5], end = [var 10, var 5])
11271 distance([line1, line2]) == 5mm
11272}
11273"
11274 );
11275
11276 ctx.close().await;
11277 mock_ctx.close().await;
11278 }
11279
11280 #[tokio::test(flavor = "multi_thread")]
11281 async fn test_distance_non_parallel_lines_lowers_to_distance() {
11282 let initial_source = "\
11283sketch(on = XY) {
11284 line(start = [var 0, var 0], end = [var 10, var 0])
11285 line(start = [var 0, var 0], end = [var 0, var 10])
11286}
11287";
11288
11289 let program = Program::parse(initial_source).unwrap().0.unwrap();
11290
11291 let mut frontend = FrontendState::new();
11292
11293 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11294 let mock_ctx = ExecutorContext::new_mock(None).await;
11295 let version = Version(0);
11296
11297 frontend.hack_set_program(&ctx, program).await.unwrap();
11298 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11299 let sketch_id = sketch_object.id;
11300 let sketch = expect_sketch(sketch_object);
11301 let line_ids = sketch
11302 .segments
11303 .iter()
11304 .copied()
11305 .filter(|segment_id| {
11306 matches!(
11307 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11308 Some(ObjectKind::Segment {
11309 segment: Segment::Line(_)
11310 })
11311 )
11312 })
11313 .collect::<Vec<_>>();
11314
11315 let constraint = Constraint::Distance(Distance {
11316 points: vec![line_ids[0].into(), line_ids[1].into()],
11317 distance: Number {
11318 value: 5.0,
11319 units: NumericSuffix::Mm,
11320 },
11321 label_position: None,
11322 source: Default::default(),
11323 });
11324 let (src_delta, _scene_delta) = frontend
11325 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11326 .await
11327 .unwrap();
11328 assert_eq!(
11329 src_delta.text.as_str(),
11330 "\
11331sketch(on = XY) {
11332 line1 = line(start = [var 4.98, var -0.07], end = [var 4.98, var 0.14])
11333 line2 = line(start = [var 0.02, var 4.3], end = [var 0.03, var 5.65])
11334 distance([line1, line2]) == 5mm
11335}
11336"
11337 );
11338
11339 ctx.close().await;
11340 mock_ctx.close().await;
11341 }
11342
11343 #[tokio::test(flavor = "multi_thread")]
11344 async fn test_horizontal_distance_two_points() {
11345 let initial_source = "\
11346sketch(on = XY) {
11347 point(at = [var 1, var 2])
11348 point(at = [var 3, var 4])
11349}
11350";
11351
11352 let program = Program::parse(initial_source).unwrap().0.unwrap();
11353
11354 let mut frontend = FrontendState::new();
11355
11356 let mock_ctx = ExecutorContext::new_mock(None).await;
11357 let version = Version(0);
11358
11359 frontend.program = program.clone();
11360 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11361 frontend.update_state_after_exec(outcome, true);
11362 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11363 let sketch_id = sketch_object.id;
11364 let sketch = expect_sketch(sketch_object);
11365 let point0_id = *sketch.segments.first().unwrap();
11366 let point1_id = *sketch.segments.get(1).unwrap();
11367 let label_position = Point2d {
11368 x: Number {
11369 value: 10.0,
11370 units: NumericSuffix::Mm,
11371 },
11372 y: Number {
11373 value: 11.0,
11374 units: NumericSuffix::Mm,
11375 },
11376 };
11377
11378 let constraint = Constraint::HorizontalDistance(Distance {
11379 points: vec![point0_id.into(), point1_id.into()],
11380 distance: Number {
11381 value: 2.0,
11382 units: NumericSuffix::Mm,
11383 },
11384 label_position: Some(label_position.clone()),
11385 source: Default::default(),
11386 });
11387 let (src_delta, scene_delta) = frontend
11388 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11389 .await
11390 .unwrap();
11391 assert_eq!(
11392 src_delta.text.as_str(),
11393 "\
11395sketch(on = XY) {
11396 point1 = point(at = [var 1, var 2])
11397 point2 = point(at = [var 3, var 4])
11398 horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11399}
11400"
11401 );
11402 assert_eq!(
11403 scene_delta.new_graph.objects.len(),
11404 5,
11405 "{:#?}",
11406 scene_delta.new_graph.objects
11407 );
11408 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11409 let sketch = expect_sketch(sketch_object);
11410 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11411 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11412 panic!("Expected constraint object");
11413 };
11414 let Constraint::HorizontalDistance(distance) = constraint else {
11415 panic!("Expected horizontal distance constraint");
11416 };
11417 assert_eq!(distance.label_position, Some(label_position));
11418
11419 mock_ctx.close().await;
11420 }
11421
11422 #[tokio::test(flavor = "multi_thread")]
11423 async fn test_radius_single_arc_segment() {
11424 let initial_source = "\
11425sketch(on = XY) {
11426 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11427}
11428";
11429
11430 let program = Program::parse(initial_source).unwrap().0.unwrap();
11431
11432 let mut frontend = FrontendState::new();
11433
11434 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11435 let mock_ctx = ExecutorContext::new_mock(None).await;
11436 let version = Version(0);
11437
11438 frontend.hack_set_program(&ctx, program).await.unwrap();
11439 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11440 let sketch_id = sketch_object.id;
11441 let sketch = expect_sketch(sketch_object);
11442 let arc_id = sketch
11444 .segments
11445 .iter()
11446 .find(|&seg_id| {
11447 let obj = frontend.scene_graph.objects.get(seg_id.0);
11448 matches!(
11449 obj.map(|o| &o.kind),
11450 Some(ObjectKind::Segment {
11451 segment: Segment::Arc(_)
11452 })
11453 )
11454 })
11455 .unwrap();
11456
11457 let constraint = Constraint::Radius(Radius {
11458 arc: *arc_id,
11459 radius: Number {
11460 value: 5.0,
11461 units: NumericSuffix::Mm,
11462 },
11463 label_position: None,
11464 source: Default::default(),
11465 });
11466 let (src_delta, scene_delta) = frontend
11467 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11468 .await
11469 .unwrap();
11470 assert_eq!(
11471 src_delta.text.as_str(),
11472 "\
11474sketch(on = XY) {
11475 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11476 radius(arc1) == 5mm
11477}
11478"
11479 );
11480 assert_eq!(
11481 scene_delta.new_graph.objects.len(),
11482 7, "{:#?}",
11484 scene_delta.new_graph.objects
11485 );
11486
11487 ctx.close().await;
11488 mock_ctx.close().await;
11489 }
11490
11491 #[tokio::test(flavor = "multi_thread")]
11492 async fn test_radius_single_arc_segment_with_label_position() {
11493 let initial_source = "\
11494sketch(on = XY) {
11495 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11496}
11497";
11498
11499 let program = Program::parse(initial_source).unwrap().0.unwrap();
11500 let mut frontend = FrontendState::new();
11501 let mock_ctx = ExecutorContext::new_mock(None).await;
11502 let version = Version(0);
11503
11504 frontend.program = program.clone();
11505 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11506 frontend.update_state_after_exec(outcome, true);
11507 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11508 let sketch_id = sketch_object.id;
11509 let sketch = expect_sketch(sketch_object);
11510 let arc_id = sketch
11511 .segments
11512 .iter()
11513 .find(|&seg_id| {
11514 let obj = frontend.scene_graph.objects.get(seg_id.0);
11515 matches!(
11516 obj.map(|o| &o.kind),
11517 Some(ObjectKind::Segment {
11518 segment: Segment::Arc(_)
11519 })
11520 )
11521 })
11522 .unwrap();
11523
11524 let label_position = Point2d {
11525 x: Number {
11526 value: 10.0,
11527 units: NumericSuffix::Mm,
11528 },
11529 y: Number {
11530 value: 11.0,
11531 units: NumericSuffix::Mm,
11532 },
11533 };
11534 let constraint = Constraint::Radius(Radius {
11535 arc: *arc_id,
11536 radius: Number {
11537 value: 5.0,
11538 units: NumericSuffix::Mm,
11539 },
11540 label_position: Some(label_position.clone()),
11541 source: Default::default(),
11542 });
11543 let (src_delta, scene_delta) = frontend
11544 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11545 .await
11546 .unwrap();
11547 assert_eq!(
11548 src_delta.text.as_str(),
11549 "\
11550sketch(on = XY) {
11551 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11552 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11553}
11554"
11555 );
11556
11557 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11558 let sketch = expect_sketch(sketch_object);
11559 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11560 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11561 panic!("Expected constraint object");
11562 };
11563 let Constraint::Radius(radius) = constraint else {
11564 panic!("Expected radius constraint");
11565 };
11566 assert_eq!(radius.label_position, Some(label_position));
11567
11568 mock_ctx.close().await;
11569 }
11570
11571 #[tokio::test(flavor = "multi_thread")]
11572 async fn test_edit_radius_constraint_label_position() {
11573 let initial_source = "\
11574sketch(on = XY) {
11575 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11576 radius(arc1) == 5mm
11577}
11578";
11579
11580 let program = Program::parse(initial_source).unwrap().0.unwrap();
11581 let mut frontend = FrontendState::new();
11582 let mock_ctx = ExecutorContext::new_mock(None).await;
11583 let version = Version(0);
11584
11585 frontend.program = program.clone();
11586 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11587 frontend.update_state_after_exec(outcome, true);
11588 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11589 let sketch_id = sketch_object.id;
11590 let sketch = expect_sketch(sketch_object);
11591 let constraint_id = sketch.constraints[0];
11592 let label_position = Point2d {
11593 x: Number {
11594 value: 10.0,
11595 units: NumericSuffix::Mm,
11596 },
11597 y: Number {
11598 value: 11.0,
11599 units: NumericSuffix::Mm,
11600 },
11601 };
11602
11603 let (src_delta, scene_delta) = frontend
11604 .edit_distance_constraint_label_position(
11605 &mock_ctx,
11606 version,
11607 sketch_id,
11608 constraint_id,
11609 label_position.clone(),
11610 vec![],
11611 )
11612 .await
11613 .unwrap();
11614 assert_eq!(
11615 src_delta.text.as_str(),
11616 "\
11617sketch(on = XY) {
11618 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11619 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11620}
11621"
11622 );
11623
11624 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11625 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11626 panic!("Expected constraint object");
11627 };
11628 let Constraint::Radius(radius) = constraint else {
11629 panic!("Expected radius constraint");
11630 };
11631 assert_eq!(radius.label_position, Some(label_position));
11632
11633 mock_ctx.close().await;
11634 }
11635
11636 #[tokio::test(flavor = "multi_thread")]
11637 async fn test_vertical_distance_two_points() {
11638 let initial_source = "\
11639sketch(on = XY) {
11640 point(at = [var 1, var 2])
11641 point(at = [var 3, var 4])
11642}
11643";
11644
11645 let program = Program::parse(initial_source).unwrap().0.unwrap();
11646
11647 let mut frontend = FrontendState::new();
11648
11649 let mock_ctx = ExecutorContext::new_mock(None).await;
11650 let version = Version(0);
11651
11652 frontend.program = program.clone();
11653 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11654 frontend.update_state_after_exec(outcome, true);
11655 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11656 let sketch_id = sketch_object.id;
11657 let sketch = expect_sketch(sketch_object);
11658 let point0_id = *sketch.segments.first().unwrap();
11659 let point1_id = *sketch.segments.get(1).unwrap();
11660 let label_position = Point2d {
11661 x: Number {
11662 value: 10.0,
11663 units: NumericSuffix::Mm,
11664 },
11665 y: Number {
11666 value: 11.0,
11667 units: NumericSuffix::Mm,
11668 },
11669 };
11670
11671 let constraint = Constraint::VerticalDistance(Distance {
11672 points: vec![point0_id.into(), point1_id.into()],
11673 distance: Number {
11674 value: 2.0,
11675 units: NumericSuffix::Mm,
11676 },
11677 label_position: Some(label_position.clone()),
11678 source: Default::default(),
11679 });
11680 let (src_delta, scene_delta) = frontend
11681 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11682 .await
11683 .unwrap();
11684 assert_eq!(
11685 src_delta.text.as_str(),
11686 "\
11688sketch(on = XY) {
11689 point1 = point(at = [var 1, var 2])
11690 point2 = point(at = [var 3, var 4])
11691 verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11692}
11693"
11694 );
11695 assert_eq!(
11696 scene_delta.new_graph.objects.len(),
11697 5,
11698 "{:#?}",
11699 scene_delta.new_graph.objects
11700 );
11701 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11702 let sketch = expect_sketch(sketch_object);
11703 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11704 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11705 panic!("Expected constraint object");
11706 };
11707 let Constraint::VerticalDistance(distance) = constraint else {
11708 panic!("Expected vertical distance constraint");
11709 };
11710 assert_eq!(distance.label_position, Some(label_position));
11711
11712 mock_ctx.close().await;
11713 }
11714
11715 #[tokio::test(flavor = "multi_thread")]
11716 async fn test_add_fixed_standalone_point() {
11717 let initial_source = "\
11718sketch(on = XY) {
11719 point(at = [var 1, var 2])
11720}
11721";
11722
11723 let program = Program::parse(initial_source).unwrap().0.unwrap();
11724
11725 let mut frontend = FrontendState::new();
11726
11727 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11728 let mock_ctx = ExecutorContext::new_mock(None).await;
11729 let version = Version(0);
11730
11731 frontend.hack_set_program(&ctx, program).await.unwrap();
11732 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11733 let sketch_id = sketch_object.id;
11734 let sketch = expect_sketch(sketch_object);
11735 let point_id = *sketch.segments.first().unwrap();
11736
11737 let (src_delta, scene_delta) = frontend
11738 .add_constraint(
11739 &mock_ctx,
11740 version,
11741 sketch_id,
11742 Constraint::Fixed(Fixed {
11743 points: vec![FixedPoint {
11744 point: point_id,
11745 position: Point2d {
11746 x: Number {
11747 value: 2.0,
11748 units: NumericSuffix::Mm,
11749 },
11750 y: Number {
11751 value: 3.0,
11752 units: NumericSuffix::Mm,
11753 },
11754 },
11755 }],
11756 }),
11757 )
11758 .await
11759 .unwrap();
11760 assert_eq!(
11761 src_delta.text.as_str(),
11762 "\
11763sketch(on = XY) {
11764 point1 = point(at = [var 2, var 3])
11765 fixed([point1, [2mm, 3mm]])
11766}
11767"
11768 );
11769 assert_eq!(
11770 scene_delta.new_graph.objects.len(),
11771 4,
11772 "{:#?}",
11773 scene_delta.new_graph.objects
11774 );
11775
11776 ctx.close().await;
11777 mock_ctx.close().await;
11778 }
11779
11780 #[tokio::test(flavor = "multi_thread")]
11781 async fn test_add_fixed_multiple_points() {
11782 let initial_source = "\
11783sketch(on = XY) {
11784 point(at = [var 1, var 2])
11785 point(at = [var 3, var 4])
11786}
11787";
11788
11789 let program = Program::parse(initial_source).unwrap().0.unwrap();
11790
11791 let mut frontend = FrontendState::new();
11792
11793 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11794 let mock_ctx = ExecutorContext::new_mock(None).await;
11795 let version = Version(0);
11796
11797 frontend.hack_set_program(&ctx, program).await.unwrap();
11798 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11799 let sketch_id = sketch_object.id;
11800 let sketch = expect_sketch(sketch_object);
11801 let point0_id = *sketch.segments.first().unwrap();
11802 let point1_id = *sketch.segments.get(1).unwrap();
11803
11804 let (src_delta, scene_delta) = frontend
11805 .add_constraint(
11806 &mock_ctx,
11807 version,
11808 sketch_id,
11809 Constraint::Fixed(Fixed {
11810 points: vec![
11811 FixedPoint {
11812 point: point0_id,
11813 position: Point2d {
11814 x: Number {
11815 value: 2.0,
11816 units: NumericSuffix::Mm,
11817 },
11818 y: Number {
11819 value: 3.0,
11820 units: NumericSuffix::Mm,
11821 },
11822 },
11823 },
11824 FixedPoint {
11825 point: point1_id,
11826 position: Point2d {
11827 x: Number {
11828 value: 4.0,
11829 units: NumericSuffix::Mm,
11830 },
11831 y: Number {
11832 value: 5.0,
11833 units: NumericSuffix::Mm,
11834 },
11835 },
11836 },
11837 ],
11838 }),
11839 )
11840 .await
11841 .unwrap();
11842 assert_eq!(
11843 src_delta.text.as_str(),
11844 "\
11845sketch(on = XY) {
11846 point1 = point(at = [var 2, var 3])
11847 point2 = point(at = [var 4, var 5])
11848 fixed([point1, [2mm, 3mm]])
11849 fixed([point2, [4mm, 5mm]])
11850}
11851"
11852 );
11853 assert_eq!(
11854 scene_delta.new_graph.objects.len(),
11855 6,
11856 "{:#?}",
11857 scene_delta.new_graph.objects
11858 );
11859
11860 ctx.close().await;
11861 mock_ctx.close().await;
11862 }
11863
11864 #[tokio::test(flavor = "multi_thread")]
11865 async fn test_add_fixed_owned_point() {
11866 let initial_source = "\
11867sketch(on = XY) {
11868 line(start = [var 1, var 2], end = [var 3, var 4])
11869}
11870";
11871
11872 let program = Program::parse(initial_source).unwrap().0.unwrap();
11873
11874 let mut frontend = FrontendState::new();
11875
11876 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11877 let mock_ctx = ExecutorContext::new_mock(None).await;
11878 let version = Version(0);
11879
11880 frontend.hack_set_program(&ctx, program).await.unwrap();
11881 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11882 let sketch_id = sketch_object.id;
11883 let sketch = expect_sketch(sketch_object);
11884 let line_start_id = *sketch.segments.first().unwrap();
11885
11886 let (src_delta, scene_delta) = frontend
11887 .add_constraint(
11888 &mock_ctx,
11889 version,
11890 sketch_id,
11891 Constraint::Fixed(Fixed {
11892 points: vec![FixedPoint {
11893 point: line_start_id,
11894 position: Point2d {
11895 x: Number {
11896 value: 2.0,
11897 units: NumericSuffix::Mm,
11898 },
11899 y: Number {
11900 value: 3.0,
11901 units: NumericSuffix::Mm,
11902 },
11903 },
11904 }],
11905 }),
11906 )
11907 .await
11908 .unwrap();
11909 assert_eq!(
11910 src_delta.text.as_str(),
11911 "\
11912sketch(on = XY) {
11913 line1 = line(start = [var 2, var 3], end = [var 3, var 4])
11914 fixed([line1.start, [2mm, 3mm]])
11915}
11916"
11917 );
11918 assert_eq!(
11919 scene_delta.new_graph.objects.len(),
11920 6,
11921 "{:#?}",
11922 scene_delta.new_graph.objects
11923 );
11924
11925 ctx.close().await;
11926 mock_ctx.close().await;
11927 }
11928
11929 #[tokio::test(flavor = "multi_thread")]
11930 async fn test_radius_error_cases() {
11931 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11932 let mock_ctx = ExecutorContext::new_mock(None).await;
11933 let version = Version(0);
11934
11935 let initial_source_point = "\
11937sketch(on = XY) {
11938 point(at = [var 1, var 2])
11939}
11940";
11941 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
11942 let mut frontend_point = FrontendState::new();
11943 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
11944 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
11945 let sketch_id_point = sketch_object_point.id;
11946 let sketch_point = expect_sketch(sketch_object_point);
11947 let point_id = *sketch_point.segments.first().unwrap();
11948
11949 let constraint_point = Constraint::Radius(Radius {
11950 arc: point_id,
11951 radius: Number {
11952 value: 5.0,
11953 units: NumericSuffix::Mm,
11954 },
11955 label_position: None,
11956 source: Default::default(),
11957 });
11958 let result_point = frontend_point
11959 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
11960 .await;
11961 assert!(result_point.is_err(), "Single point should error for radius");
11962
11963 let initial_source_line = "\
11965sketch(on = XY) {
11966 line(start = [var 1, var 2], end = [var 3, var 4])
11967}
11968";
11969 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
11970 let mut frontend_line = FrontendState::new();
11971 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
11972 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
11973 let sketch_id_line = sketch_object_line.id;
11974 let sketch_line = expect_sketch(sketch_object_line);
11975 let line_id = *sketch_line.segments.first().unwrap();
11976
11977 let constraint_line = Constraint::Radius(Radius {
11978 arc: line_id,
11979 radius: Number {
11980 value: 5.0,
11981 units: NumericSuffix::Mm,
11982 },
11983 label_position: None,
11984 source: Default::default(),
11985 });
11986 let result_line = frontend_line
11987 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
11988 .await;
11989 assert!(result_line.is_err(), "Single line segment should error for radius");
11990
11991 ctx.close().await;
11992 mock_ctx.close().await;
11993 }
11994
11995 #[tokio::test(flavor = "multi_thread")]
11996 async fn test_diameter_single_arc_segment() {
11997 let initial_source = "\
11998sketch(on = XY) {
11999 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12000}
12001";
12002
12003 let program = Program::parse(initial_source).unwrap().0.unwrap();
12004
12005 let mut frontend = FrontendState::new();
12006
12007 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12008 let mock_ctx = ExecutorContext::new_mock(None).await;
12009 let version = Version(0);
12010
12011 frontend.hack_set_program(&ctx, program).await.unwrap();
12012 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12013 let sketch_id = sketch_object.id;
12014 let sketch = expect_sketch(sketch_object);
12015 let arc_id = sketch
12017 .segments
12018 .iter()
12019 .find(|&seg_id| {
12020 let obj = frontend.scene_graph.objects.get(seg_id.0);
12021 matches!(
12022 obj.map(|o| &o.kind),
12023 Some(ObjectKind::Segment {
12024 segment: Segment::Arc(_)
12025 })
12026 )
12027 })
12028 .unwrap();
12029
12030 let constraint = Constraint::Diameter(Diameter {
12031 arc: *arc_id,
12032 diameter: Number {
12033 value: 10.0,
12034 units: NumericSuffix::Mm,
12035 },
12036 label_position: None,
12037 source: Default::default(),
12038 });
12039 let (src_delta, scene_delta) = frontend
12040 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12041 .await
12042 .unwrap();
12043 assert_eq!(
12044 src_delta.text.as_str(),
12045 "\
12047sketch(on = XY) {
12048 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12049 diameter(arc1) == 10mm
12050}
12051"
12052 );
12053 assert_eq!(
12054 scene_delta.new_graph.objects.len(),
12055 7, "{:#?}",
12057 scene_delta.new_graph.objects
12058 );
12059
12060 ctx.close().await;
12061 mock_ctx.close().await;
12062 }
12063
12064 #[tokio::test(flavor = "multi_thread")]
12065 async fn test_diameter_single_arc_segment_with_label_position() {
12066 let initial_source = "\
12067sketch(on = XY) {
12068 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12069}
12070";
12071
12072 let program = Program::parse(initial_source).unwrap().0.unwrap();
12073 let mut frontend = FrontendState::new();
12074 let mock_ctx = ExecutorContext::new_mock(None).await;
12075 let version = Version(0);
12076
12077 frontend.program = program.clone();
12078 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12079 frontend.update_state_after_exec(outcome, true);
12080 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12081 let sketch_id = sketch_object.id;
12082 let sketch = expect_sketch(sketch_object);
12083 let arc_id = sketch
12084 .segments
12085 .iter()
12086 .find(|&seg_id| {
12087 let obj = frontend.scene_graph.objects.get(seg_id.0);
12088 matches!(
12089 obj.map(|o| &o.kind),
12090 Some(ObjectKind::Segment {
12091 segment: Segment::Arc(_)
12092 })
12093 )
12094 })
12095 .unwrap();
12096
12097 let label_position = Point2d {
12098 x: Number {
12099 value: 10.0,
12100 units: NumericSuffix::Mm,
12101 },
12102 y: Number {
12103 value: 11.0,
12104 units: NumericSuffix::Mm,
12105 },
12106 };
12107 let constraint = Constraint::Diameter(Diameter {
12108 arc: *arc_id,
12109 diameter: Number {
12110 value: 10.0,
12111 units: NumericSuffix::Mm,
12112 },
12113 label_position: Some(label_position.clone()),
12114 source: Default::default(),
12115 });
12116 let (src_delta, scene_delta) = frontend
12117 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12118 .await
12119 .unwrap();
12120 assert_eq!(
12121 src_delta.text.as_str(),
12122 "\
12123sketch(on = XY) {
12124 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12125 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12126}
12127"
12128 );
12129
12130 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12131 let sketch = expect_sketch(sketch_object);
12132 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12133 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12134 panic!("Expected constraint object");
12135 };
12136 let Constraint::Diameter(diameter) = constraint else {
12137 panic!("Expected diameter constraint");
12138 };
12139 assert_eq!(diameter.label_position, Some(label_position));
12140
12141 mock_ctx.close().await;
12142 }
12143
12144 #[tokio::test(flavor = "multi_thread")]
12145 async fn test_edit_diameter_constraint_label_position() {
12146 let initial_source = "\
12147sketch(on = XY) {
12148 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12149 diameter(arc1) == 10mm
12150}
12151";
12152
12153 let program = Program::parse(initial_source).unwrap().0.unwrap();
12154 let mut frontend = FrontendState::new();
12155 let mock_ctx = ExecutorContext::new_mock(None).await;
12156 let version = Version(0);
12157
12158 frontend.program = program.clone();
12159 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12160 frontend.update_state_after_exec(outcome, true);
12161 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12162 let sketch_id = sketch_object.id;
12163 let sketch = expect_sketch(sketch_object);
12164 let constraint_id = sketch.constraints[0];
12165 let label_position = Point2d {
12166 x: Number {
12167 value: 10.0,
12168 units: NumericSuffix::Mm,
12169 },
12170 y: Number {
12171 value: 11.0,
12172 units: NumericSuffix::Mm,
12173 },
12174 };
12175
12176 let (src_delta, scene_delta) = frontend
12177 .edit_distance_constraint_label_position(
12178 &mock_ctx,
12179 version,
12180 sketch_id,
12181 constraint_id,
12182 label_position.clone(),
12183 vec![],
12184 )
12185 .await
12186 .unwrap();
12187 assert_eq!(
12188 src_delta.text.as_str(),
12189 "\
12190sketch(on = XY) {
12191 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12192 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12193}
12194"
12195 );
12196
12197 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12198 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12199 panic!("Expected constraint object");
12200 };
12201 let Constraint::Diameter(diameter) = constraint else {
12202 panic!("Expected diameter constraint");
12203 };
12204 assert_eq!(diameter.label_position, Some(label_position));
12205
12206 mock_ctx.close().await;
12207 }
12208
12209 #[tokio::test(flavor = "multi_thread")]
12210 async fn test_diameter_error_cases() {
12211 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12212 let mock_ctx = ExecutorContext::new_mock(None).await;
12213 let version = Version(0);
12214
12215 let initial_source_point = "\
12217sketch(on = XY) {
12218 point(at = [var 1, var 2])
12219}
12220";
12221 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12222 let mut frontend_point = FrontendState::new();
12223 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12224 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12225 let sketch_id_point = sketch_object_point.id;
12226 let sketch_point = expect_sketch(sketch_object_point);
12227 let point_id = *sketch_point.segments.first().unwrap();
12228
12229 let constraint_point = Constraint::Diameter(Diameter {
12230 arc: point_id,
12231 diameter: Number {
12232 value: 10.0,
12233 units: NumericSuffix::Mm,
12234 },
12235 label_position: None,
12236 source: Default::default(),
12237 });
12238 let result_point = frontend_point
12239 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12240 .await;
12241 assert!(result_point.is_err(), "Single point should error for diameter");
12242
12243 let initial_source_line = "\
12245sketch(on = XY) {
12246 line(start = [var 1, var 2], end = [var 3, var 4])
12247}
12248";
12249 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12250 let mut frontend_line = FrontendState::new();
12251 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12252 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12253 let sketch_id_line = sketch_object_line.id;
12254 let sketch_line = expect_sketch(sketch_object_line);
12255 let line_id = *sketch_line.segments.first().unwrap();
12256
12257 let constraint_line = Constraint::Diameter(Diameter {
12258 arc: line_id,
12259 diameter: Number {
12260 value: 10.0,
12261 units: NumericSuffix::Mm,
12262 },
12263 label_position: None,
12264 source: Default::default(),
12265 });
12266 let result_line = frontend_line
12267 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12268 .await;
12269 assert!(result_line.is_err(), "Single line segment should error for diameter");
12270
12271 ctx.close().await;
12272 mock_ctx.close().await;
12273 }
12274
12275 #[tokio::test(flavor = "multi_thread")]
12276 async fn test_line_horizontal() {
12277 let initial_source = "\
12278sketch(on = XY) {
12279 line(start = [var 1, var 2], end = [var 3, var 4])
12280}
12281";
12282
12283 let program = Program::parse(initial_source).unwrap().0.unwrap();
12284
12285 let mut frontend = FrontendState::new();
12286
12287 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12288 let mock_ctx = ExecutorContext::new_mock(None).await;
12289 let version = Version(0);
12290
12291 frontend.hack_set_program(&ctx, program).await.unwrap();
12292 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12293 let sketch_id = sketch_object.id;
12294 let sketch = expect_sketch(sketch_object);
12295 let line1_id = *sketch.segments.get(2).unwrap();
12296
12297 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
12298 let (src_delta, scene_delta) = frontend
12299 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12300 .await
12301 .unwrap();
12302 assert_eq!(
12303 src_delta.text.as_str(),
12304 "\
12305sketch(on = XY) {
12306 line1 = line(start = [var 1, var 3], end = [var 3, var 3])
12307 horizontal(line1)
12308}
12309"
12310 );
12311 assert_eq!(
12312 scene_delta.new_graph.objects.len(),
12313 6,
12314 "{:#?}",
12315 scene_delta.new_graph.objects
12316 );
12317
12318 ctx.close().await;
12319 mock_ctx.close().await;
12320 }
12321
12322 #[tokio::test(flavor = "multi_thread")]
12323 async fn test_control_point_spline_edge_horizontal() {
12324 let initial_source = "\
12325@settings(experimentalFeatures = allow)
12326splineSketch = sketch(on = XY) {
12327 controlPointSpline1 = controlPointSpline(points = [
12328 [var 0mm, var 0mm],
12329 [var 10mm, var 20mm],
12330 [var 20mm, var 0mm],
12331 ])
12332}
12333";
12334
12335 let program = Program::parse(initial_source).unwrap().0.unwrap();
12336
12337 let mut frontend = FrontendState::new();
12338
12339 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12340 let mock_ctx = ExecutorContext::new_mock(None).await;
12341 let version = Version(0);
12342
12343 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12344 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12345 let sketch_id = sketch_object.id;
12346 let sketch = expect_sketch(sketch_object);
12347 let spline_id = sketch
12348 .segments
12349 .iter()
12350 .copied()
12351 .find(|seg_id| {
12352 matches!(
12353 &frontend.scene_graph.objects[seg_id.0].kind,
12354 ObjectKind::Segment {
12355 segment: Segment::ControlPointSpline(_)
12356 }
12357 )
12358 })
12359 .expect("Expected a control point spline segment in sketch");
12360 let edge_id = frontend
12361 .scene_graph
12362 .objects
12363 .iter()
12364 .find_map(|obj| match &obj.kind {
12365 ObjectKind::Segment {
12366 segment: Segment::Line(line),
12367 } if line.owner == Some(spline_id) => Some(obj.id),
12368 _ => None,
12369 })
12370 .expect("Expected an owned control-polygon edge");
12371
12372 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
12373 let (src_delta, _) = frontend
12374 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12375 .await
12376 .unwrap();
12377 assert!(
12378 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
12379 "Expected horizontal constraint on spline edge, got: {}",
12380 src_delta.text
12381 );
12382
12383 ctx.close().await;
12384 mock_ctx.close().await;
12385 }
12386
12387 #[tokio::test(flavor = "multi_thread")]
12388 async fn test_control_point_spline_edge_angle() {
12389 let initial_source = "\
12390@settings(experimentalFeatures = allow)
12391splineSketch = sketch(on = XY) {
12392 controlPointSpline1 = controlPointSpline(points = [
12393 [var 0mm, var 0mm],
12394 [var 10mm, var 20mm],
12395 [var 20mm, var 0mm],
12396 ])
12397
12398 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
12399}
12400";
12401
12402 let program = Program::parse(initial_source).unwrap().0.unwrap();
12403
12404 let mut frontend = FrontendState::new();
12405
12406 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12407 let mock_ctx = ExecutorContext::new_mock(None).await;
12408 let version = Version(0);
12409
12410 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12411 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12412 let sketch_id = sketch_object.id;
12413 let sketch = expect_sketch(sketch_object);
12414 let spline_id = sketch
12415 .segments
12416 .iter()
12417 .copied()
12418 .find(|seg_id| {
12419 matches!(
12420 &frontend.scene_graph.objects[seg_id.0].kind,
12421 ObjectKind::Segment {
12422 segment: Segment::ControlPointSpline(_)
12423 }
12424 )
12425 })
12426 .expect("Expected a control point spline segment in sketch");
12427 let edge_id = frontend
12428 .scene_graph
12429 .objects
12430 .iter()
12431 .find_map(|obj| match &obj.kind {
12432 ObjectKind::Segment {
12433 segment: Segment::Line(line),
12434 } if line.owner == Some(spline_id) => Some(obj.id),
12435 _ => None,
12436 })
12437 .expect("Expected an owned control-polygon edge");
12438 let line1_id = frontend
12439 .scene_graph
12440 .objects
12441 .iter()
12442 .find_map(|obj| match &obj.kind {
12443 ObjectKind::Segment {
12444 segment: Segment::Line(line),
12445 } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
12446 _ => None,
12447 })
12448 .or_else(|| {
12449 sketch.segments.iter().copied().find(|seg_id| {
12450 matches!(
12451 &frontend.scene_graph.objects[seg_id.0].kind,
12452 ObjectKind::Segment {
12453 segment: Segment::Line(line),
12454 } if line.owner.is_none()
12455 )
12456 })
12457 })
12458 .expect("Expected a standalone line segment in sketch");
12459
12460 let constraint = Constraint::Angle(Angle {
12461 lines: vec![line1_id, edge_id],
12462 angle: Number {
12463 value: 30.0,
12464 units: NumericSuffix::Deg,
12465 },
12466 source: Default::default(),
12467 });
12468 let (src_delta, _) = frontend
12469 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12470 .await
12471 .unwrap();
12472 assert!(
12473 src_delta
12474 .text
12475 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
12476 "Expected angle constraint on spline edge, got: {}",
12477 src_delta.text
12478 );
12479
12480 ctx.close().await;
12481 mock_ctx.close().await;
12482 }
12483
12484 #[tokio::test(flavor = "multi_thread")]
12485 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
12486 let initial_source = "\
12487@settings(experimentalFeatures = allow)
12488splineSketch = sketch(on = XY) {
12489 spline1 = controlPointSpline(points = [
12490 [var 0mm, var 0mm],
12491 [var 10mm, var 20mm],
12492 [var 20mm, var 0mm],
12493 ])
12494 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
12495 coincident([spline1.controls[1], spline1.edges[0]])
12496 coincident([spline1.controls[0], line1])
12497}
12498";
12499
12500 let program = Program::parse(initial_source).unwrap().0.unwrap();
12501
12502 let mut frontend = FrontendState::new();
12503
12504 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12505 let mock_ctx = ExecutorContext::new_mock(None).await;
12506
12507 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12508
12509 let ui_scene_graph = frontend.scene_graph_for_ui();
12510 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
12511 let sketch = expect_sketch(sketch_object);
12512
12513 assert_eq!(
12514 sketch.constraints.len(),
12515 1,
12516 "Expected only the external coincident constraint to remain visible in the UI scene graph"
12517 );
12518
12519 let visible_constraints = ui_scene_graph
12520 .objects
12521 .iter()
12522 .filter_map(|object| match &object.kind {
12523 ObjectKind::Constraint {
12524 constraint: Constraint::Coincident(coincident),
12525 } => Some(coincident.clone()),
12526 _ => None,
12527 })
12528 .collect::<Vec<_>>();
12529
12530 assert_eq!(
12531 visible_constraints.len(),
12532 1,
12533 "Expected only one coincident constraint object in the UI scene graph"
12534 );
12535 assert_eq!(
12536 visible_constraints[0].get_segments().len(),
12537 2,
12538 "Expected the remaining visible coincident constraint to reference two segments"
12539 );
12540
12541 ctx.close().await;
12542 mock_ctx.close().await;
12543 }
12544
12545 #[tokio::test(flavor = "multi_thread")]
12546 async fn test_edit_control_point_spline_can_append_control_point() {
12547 let initial_source = "\
12548@settings(experimentalFeatures = allow)
12549splineSketch = sketch(on = XY) {
12550 controlPointSpline(points = [
12551 [var 0mm, var 0mm],
12552 [var 10mm, var 20mm],
12553 [var 20mm, var 0mm],
12554 ])
12555}
12556";
12557
12558 let program = Program::parse(initial_source).unwrap().0.unwrap();
12559
12560 let mut frontend = FrontendState::new();
12561
12562 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12563 let mock_ctx = ExecutorContext::new_mock(None).await;
12564 let version = Version(0);
12565
12566 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12567 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12568 let sketch_id = sketch_object.id;
12569 let sketch = expect_sketch(sketch_object);
12570 let spline_id = sketch
12571 .segments
12572 .iter()
12573 .copied()
12574 .find(|seg_id| {
12575 matches!(
12576 &frontend.scene_graph.objects[seg_id.0].kind,
12577 ObjectKind::Segment {
12578 segment: Segment::ControlPointSpline(_)
12579 }
12580 )
12581 })
12582 .expect("Expected a control point spline segment in sketch");
12583
12584 let ctor = ControlPointSplineCtor {
12585 points: vec![
12586 Point2d {
12587 x: Expr::Var(Number {
12588 value: 0.0,
12589 units: NumericSuffix::Mm,
12590 }),
12591 y: Expr::Var(Number {
12592 value: 0.0,
12593 units: NumericSuffix::Mm,
12594 }),
12595 },
12596 Point2d {
12597 x: Expr::Var(Number {
12598 value: 10.0,
12599 units: NumericSuffix::Mm,
12600 }),
12601 y: Expr::Var(Number {
12602 value: 20.0,
12603 units: NumericSuffix::Mm,
12604 }),
12605 },
12606 Point2d {
12607 x: Expr::Var(Number {
12608 value: 20.0,
12609 units: NumericSuffix::Mm,
12610 }),
12611 y: Expr::Var(Number {
12612 value: 0.0,
12613 units: NumericSuffix::Mm,
12614 }),
12615 },
12616 Point2d {
12617 x: Expr::Var(Number {
12618 value: 30.0,
12619 units: NumericSuffix::Mm,
12620 }),
12621 y: Expr::Var(Number {
12622 value: 10.0,
12623 units: NumericSuffix::Mm,
12624 }),
12625 },
12626 ],
12627 construction: None,
12628 };
12629
12630 let segments = vec![ExistingSegmentCtor {
12631 id: spline_id,
12632 ctor: SegmentCtor::ControlPointSpline(ctor),
12633 }];
12634 let (src_delta, scene_delta) = frontend
12635 .edit_segments(&mock_ctx, version, sketch_id, segments)
12636 .await
12637 .unwrap();
12638
12639 assert!(
12640 src_delta.text.contains("[var 30mm, var 10mm]"),
12641 "Expected appended spline control point in source, got: {}",
12642 src_delta.text
12643 );
12644
12645 assert!(
12646 scene_delta.invalidates_ids,
12647 "Expected appending a spline control point to invalidate ids"
12648 );
12649 let updated_spline = scene_delta
12650 .new_graph
12651 .objects
12652 .iter()
12653 .find_map(|obj| match &obj.kind {
12654 ObjectKind::Segment {
12655 segment: Segment::ControlPointSpline(updated_spline),
12656 } if updated_spline.controls.len() == 4 => Some(updated_spline),
12657 _ => None,
12658 })
12659 .expect("Expected edited scene graph to contain a four-point control point spline");
12660 assert_eq!(
12661 updated_spline.controls.len(),
12662 4,
12663 "Expected edited spline to expose four control points"
12664 );
12665
12666 ctx.close().await;
12667 mock_ctx.close().await;
12668 }
12669
12670 #[tokio::test(flavor = "multi_thread")]
12671 async fn test_line_vertical() {
12672 let initial_source = "\
12673sketch(on = XY) {
12674 line(start = [var 1, var 2], end = [var 3, var 4])
12675}
12676";
12677
12678 let program = Program::parse(initial_source).unwrap().0.unwrap();
12679
12680 let mut frontend = FrontendState::new();
12681
12682 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12683 let mock_ctx = ExecutorContext::new_mock(None).await;
12684 let version = Version(0);
12685
12686 frontend.hack_set_program(&ctx, program).await.unwrap();
12687 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12688 let sketch_id = sketch_object.id;
12689 let sketch = expect_sketch(sketch_object);
12690 let line1_id = *sketch.segments.get(2).unwrap();
12691
12692 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
12693 let (src_delta, scene_delta) = frontend
12694 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12695 .await
12696 .unwrap();
12697 assert_eq!(
12698 src_delta.text.as_str(),
12699 "\
12700sketch(on = XY) {
12701 line1 = line(start = [var 2, var 2], end = [var 2, var 4])
12702 vertical(line1)
12703}
12704"
12705 );
12706 assert_eq!(
12707 scene_delta.new_graph.objects.len(),
12708 6,
12709 "{:#?}",
12710 scene_delta.new_graph.objects
12711 );
12712
12713 ctx.close().await;
12714 mock_ctx.close().await;
12715 }
12716
12717 #[tokio::test(flavor = "multi_thread")]
12718 async fn test_points_vertical() {
12719 let initial_source = "\
12720sketch001 = sketch(on = XY) {
12721 p0 = point(at = [var -2.23mm, var 3.1mm])
12722 pf = point(at = [4, 4])
12723}
12724";
12725
12726 let program = Program::parse(initial_source).unwrap().0.unwrap();
12727
12728 let mut frontend = FrontendState::new();
12729
12730 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12731 let mock_ctx = ExecutorContext::new_mock(None).await;
12732 let version = Version(0);
12733
12734 frontend.hack_set_program(&ctx, program).await.unwrap();
12735 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12736 let sketch_id = sketch_object.id;
12737 let sketch = expect_sketch(sketch_object);
12738 let point_ids = vec![
12739 sketch.segments.first().unwrap().to_owned(),
12740 sketch.segments.get(1).unwrap().to_owned(),
12741 ];
12742
12743 let constraint = Constraint::Vertical(Vertical::Points {
12744 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12745 });
12746 let (src_delta, scene_delta) = frontend
12747 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12748 .await
12749 .unwrap();
12750 assert_eq!(
12751 src_delta.text.as_str(),
12752 "\
12753sketch001 = sketch(on = XY) {
12754 p0 = point(at = [var 4mm, var 3.1mm])
12755 pf = point(at = [4, 4])
12756 vertical([p0, pf])
12757}
12758"
12759 );
12760 assert_eq!(
12761 scene_delta.new_graph.objects.len(),
12762 5,
12763 "{:#?}",
12764 scene_delta.new_graph.objects
12765 );
12766
12767 ctx.close().await;
12768 mock_ctx.close().await;
12769 }
12770
12771 #[tokio::test(flavor = "multi_thread")]
12772 async fn test_points_horizontal() {
12773 let initial_source = "\
12774sketch001 = sketch(on = XY) {
12775 p0 = point(at = [var -2.23mm, var 3.1mm])
12776 pf = point(at = [4, 4])
12777}
12778";
12779
12780 let program = Program::parse(initial_source).unwrap().0.unwrap();
12781
12782 let mut frontend = FrontendState::new();
12783
12784 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12785 let mock_ctx = ExecutorContext::new_mock(None).await;
12786 let version = Version(0);
12787
12788 frontend.hack_set_program(&ctx, program).await.unwrap();
12789 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12790 let sketch_id = sketch_object.id;
12791 let sketch = expect_sketch(sketch_object);
12792 let point_ids = vec![
12793 sketch.segments.first().unwrap().to_owned(),
12794 sketch.segments.get(1).unwrap().to_owned(),
12795 ];
12796
12797 let constraint = Constraint::Horizontal(Horizontal::Points {
12798 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12799 });
12800 let (src_delta, scene_delta) = frontend
12801 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12802 .await
12803 .unwrap();
12804 assert_eq!(
12805 src_delta.text.as_str(),
12806 "\
12807sketch001 = sketch(on = XY) {
12808 p0 = point(at = [var -2.23mm, var 4mm])
12809 pf = point(at = [4, 4])
12810 horizontal([p0, pf])
12811}
12812"
12813 );
12814 assert_eq!(
12815 scene_delta.new_graph.objects.len(),
12816 5,
12817 "{:#?}",
12818 scene_delta.new_graph.objects
12819 );
12820
12821 ctx.close().await;
12822 mock_ctx.close().await;
12823 }
12824
12825 #[tokio::test(flavor = "multi_thread")]
12826 async fn test_point_horizontal_with_origin() {
12827 let initial_source = "\
12828sketch001 = sketch(on = XY) {
12829 p0 = point(at = [var -2.23mm, var 3.1mm])
12830}
12831";
12832
12833 let program = Program::parse(initial_source).unwrap().0.unwrap();
12834
12835 let mut frontend = FrontendState::new();
12836
12837 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12838 let mock_ctx = ExecutorContext::new_mock(None).await;
12839 let version = Version(0);
12840
12841 frontend.hack_set_program(&ctx, program).await.unwrap();
12842 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12843 let sketch_id = sketch_object.id;
12844 let sketch = expect_sketch(sketch_object);
12845 let point_id = *sketch.segments.first().unwrap();
12846
12847 let constraint = Constraint::Horizontal(Horizontal::Points {
12848 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
12849 });
12850 let (src_delta, scene_delta) = frontend
12851 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12852 .await
12853 .unwrap();
12854 assert_eq!(
12855 src_delta.text.as_str(),
12856 "\
12857sketch001 = sketch(on = XY) {
12858 p0 = point(at = [var -2.23mm, var 0mm])
12859 horizontal([p0, ORIGIN])
12860}
12861"
12862 );
12863 assert_eq!(
12864 scene_delta.new_graph.objects.len(),
12865 4,
12866 "{:#?}",
12867 scene_delta.new_graph.objects
12868 );
12869
12870 ctx.close().await;
12871 mock_ctx.close().await;
12872 }
12873
12874 #[tokio::test(flavor = "multi_thread")]
12875 async fn test_lines_equal_length() {
12876 let initial_source = "\
12877sketch(on = XY) {
12878 line(start = [var 1, var 2], end = [var 3, var 4])
12879 line(start = [var 5, var 6], end = [var 7, var 8])
12880}
12881";
12882
12883 let program = Program::parse(initial_source).unwrap().0.unwrap();
12884
12885 let mut frontend = FrontendState::new();
12886
12887 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12888 let mock_ctx = ExecutorContext::new_mock(None).await;
12889 let version = Version(0);
12890
12891 frontend.hack_set_program(&ctx, program).await.unwrap();
12892 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12893 let sketch_id = sketch_object.id;
12894 let sketch = expect_sketch(sketch_object);
12895 let line1_id = *sketch.segments.get(2).unwrap();
12896 let line2_id = *sketch.segments.get(5).unwrap();
12897
12898 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
12899 lines: vec![line1_id, line2_id],
12900 });
12901 let (src_delta, scene_delta) = frontend
12902 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12903 .await
12904 .unwrap();
12905 assert_eq!(
12906 src_delta.text.as_str(),
12907 "\
12908sketch(on = XY) {
12909 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12910 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12911 equalLength([line1, line2])
12912}
12913"
12914 );
12915 assert_eq!(
12916 scene_delta.new_graph.objects.len(),
12917 9,
12918 "{:#?}",
12919 scene_delta.new_graph.objects
12920 );
12921
12922 ctx.close().await;
12923 mock_ctx.close().await;
12924 }
12925
12926 #[tokio::test(flavor = "multi_thread")]
12927 async fn test_add_constraint_multi_line_equal_length() {
12928 let initial_source = "\
12929sketch(on = XY) {
12930 line(start = [var 1, var 2], end = [var 3, var 4])
12931 line(start = [var 5, var 6], end = [var 7, var 8])
12932 line(start = [var 9, var 10], end = [var 11, var 12])
12933}
12934";
12935
12936 let program = Program::parse(initial_source).unwrap().0.unwrap();
12937
12938 let mut frontend = FrontendState::new();
12939 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12940 let mock_ctx = ExecutorContext::new_mock(None).await;
12941 let version = Version(0);
12942
12943 frontend.hack_set_program(&ctx, program).await.unwrap();
12944 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12945 let sketch_id = sketch_object.id;
12946 let sketch = expect_sketch(sketch_object);
12947 let line1_id = *sketch.segments.get(2).unwrap();
12948 let line2_id = *sketch.segments.get(5).unwrap();
12949 let line3_id = *sketch.segments.get(8).unwrap();
12950
12951 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
12952 lines: vec![line1_id, line2_id, line3_id],
12953 });
12954 let (src_delta, scene_delta) = frontend
12955 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12956 .await
12957 .unwrap();
12958 assert_eq!(
12959 src_delta.text.as_str(),
12960 "\
12961sketch(on = XY) {
12962 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12963 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12964 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
12965 equalLength([line1, line2, line3])
12966}
12967"
12968 );
12969 let constraints = scene_delta
12970 .new_graph
12971 .objects
12972 .iter()
12973 .filter_map(|obj| {
12974 let ObjectKind::Constraint { constraint } = &obj.kind else {
12975 return None;
12976 };
12977 Some(constraint)
12978 })
12979 .collect::<Vec<_>>();
12980
12981 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
12982 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
12983 panic!("expected equal length constraint, got {:?}", constraints[0]);
12984 };
12985 assert_eq!(lines_equal_length.lines.len(), 3);
12986
12987 ctx.close().await;
12988 mock_ctx.close().await;
12989 }
12990
12991 #[tokio::test(flavor = "multi_thread")]
12992 async fn test_lines_parallel() {
12993 let initial_source = "\
12994sketch(on = XY) {
12995 line(start = [var 1, var 2], end = [var 3, var 4])
12996 line(start = [var 5, var 6], end = [var 7, var 8])
12997}
12998";
12999
13000 let program = Program::parse(initial_source).unwrap().0.unwrap();
13001
13002 let mut frontend = FrontendState::new();
13003
13004 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13005 let mock_ctx = ExecutorContext::new_mock(None).await;
13006 let version = Version(0);
13007
13008 frontend.hack_set_program(&ctx, program).await.unwrap();
13009 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13010 let sketch_id = sketch_object.id;
13011 let sketch = expect_sketch(sketch_object);
13012 let line1_id = *sketch.segments.get(2).unwrap();
13013 let line2_id = *sketch.segments.get(5).unwrap();
13014
13015 let constraint = Constraint::Parallel(Parallel {
13016 lines: vec![line1_id, line2_id],
13017 });
13018 let (src_delta, scene_delta) = frontend
13019 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13020 .await
13021 .unwrap();
13022 assert_eq!(
13023 src_delta.text.as_str(),
13024 "\
13025sketch(on = XY) {
13026 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13027 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13028 parallel([line1, line2])
13029}
13030"
13031 );
13032 assert_eq!(
13033 scene_delta.new_graph.objects.len(),
13034 9,
13035 "{:#?}",
13036 scene_delta.new_graph.objects
13037 );
13038
13039 ctx.close().await;
13040 mock_ctx.close().await;
13041 }
13042
13043 #[tokio::test(flavor = "multi_thread")]
13044 async fn test_lines_parallel_multiline() {
13045 let initial_source = "\
13046sketch(on = XY) {
13047 line(start = [var 1, var 2], end = [var 3, var 4])
13048 line(start = [var 5, var 6], end = [var 7, var 8])
13049 line(start = [var 9, var 10], end = [var 11, var 12])
13050}
13051";
13052
13053 let program = Program::parse(initial_source).unwrap().0.unwrap();
13054
13055 let mut frontend = FrontendState::new();
13056
13057 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13058 let mock_ctx = ExecutorContext::new_mock(None).await;
13059 let version = Version(0);
13060
13061 frontend.hack_set_program(&ctx, program).await.unwrap();
13062 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13063 let sketch_id = sketch_object.id;
13064 let sketch = expect_sketch(sketch_object);
13065 let line1_id = *sketch.segments.get(2).unwrap();
13066 let line2_id = *sketch.segments.get(5).unwrap();
13067 let line3_id = *sketch.segments.get(8).unwrap();
13068
13069 let constraint = Constraint::Parallel(Parallel {
13070 lines: vec![line1_id, line2_id, line3_id],
13071 });
13072 let (src_delta, scene_delta) = frontend
13073 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13074 .await
13075 .unwrap();
13076 assert_eq!(
13077 src_delta.text.as_str(),
13078 "\
13079sketch(on = XY) {
13080 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13081 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13082 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
13083 parallel([line1, line2, line3])
13084}
13085"
13086 );
13087
13088 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13089 let sketch = expect_sketch(sketch_object);
13090 assert_eq!(sketch.constraints.len(), 1);
13091
13092 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13093 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13094 panic!("Expected constraint object");
13095 };
13096 let Constraint::Parallel(parallel) = constraint else {
13097 panic!("Expected parallel constraint");
13098 };
13099 assert_eq!(parallel.lines.len(), 3);
13100
13101 ctx.close().await;
13102 mock_ctx.close().await;
13103 }
13104
13105 #[tokio::test(flavor = "multi_thread")]
13106 async fn test_lines_perpendicular() {
13107 let initial_source = "\
13108sketch(on = XY) {
13109 line(start = [var 1, var 2], end = [var 3, var 4])
13110 line(start = [var 5, var 6], end = [var 7, var 8])
13111}
13112";
13113
13114 let program = Program::parse(initial_source).unwrap().0.unwrap();
13115
13116 let mut frontend = FrontendState::new();
13117
13118 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13119 let mock_ctx = ExecutorContext::new_mock(None).await;
13120 let version = Version(0);
13121
13122 frontend.hack_set_program(&ctx, program).await.unwrap();
13123 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13124 let sketch_id = sketch_object.id;
13125 let sketch = expect_sketch(sketch_object);
13126 let line1_id = *sketch.segments.get(2).unwrap();
13127 let line2_id = *sketch.segments.get(5).unwrap();
13128
13129 let constraint = Constraint::Perpendicular(Perpendicular {
13130 lines: vec![line1_id, line2_id],
13131 });
13132 let (src_delta, scene_delta) = frontend
13133 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13134 .await
13135 .unwrap();
13136 assert_eq!(
13137 src_delta.text.as_str(),
13138 "\
13139sketch(on = XY) {
13140 line1 = line(start = [var 2, var 3], end = [var 2, var 3])
13141 line2 = line(start = [var 6, var 7], end = [var 6, var 7])
13142 perpendicular([line1, line2])
13143}
13144"
13145 );
13146 assert_eq!(
13147 scene_delta.new_graph.objects.len(),
13148 9,
13149 "{:#?}",
13150 scene_delta.new_graph.objects
13151 );
13152
13153 ctx.close().await;
13154 mock_ctx.close().await;
13155 }
13156
13157 #[tokio::test(flavor = "multi_thread")]
13158 async fn test_lines_angle() {
13159 let initial_source = "\
13160sketch(on = XY) {
13161 line(start = [var 1, var 2], end = [var 3, var 4])
13162 line(start = [var 5, var 6], end = [var 7, var 8])
13163}
13164";
13165
13166 let program = Program::parse(initial_source).unwrap().0.unwrap();
13167
13168 let mut frontend = FrontendState::new();
13169
13170 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13171 let mock_ctx = ExecutorContext::new_mock(None).await;
13172 let version = Version(0);
13173
13174 frontend.hack_set_program(&ctx, program).await.unwrap();
13175 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13176 let sketch_id = sketch_object.id;
13177 let sketch = expect_sketch(sketch_object);
13178 let line1_id = *sketch.segments.get(2).unwrap();
13179 let line2_id = *sketch.segments.get(5).unwrap();
13180
13181 let constraint = Constraint::Angle(Angle {
13182 lines: vec![line1_id, line2_id],
13183 angle: Number {
13184 value: 30.0,
13185 units: NumericSuffix::Deg,
13186 },
13187 source: Default::default(),
13188 });
13189 let (src_delta, scene_delta) = frontend
13190 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13191 .await
13192 .unwrap();
13193 assert_eq!(
13194 src_delta.text.as_str(),
13195 "\
13197sketch(on = XY) {
13198 line1 = line(start = [var 0.9, var 2.36], end = [var 3.1, var 3.64])
13199 line2 = line(start = [var 5.36, var 5.9], end = [var 6.64, var 8.1])
13200 angle([line1, line2]) == 30deg
13201}
13202"
13203 );
13204 assert_eq!(
13205 scene_delta.new_graph.objects.len(),
13206 9,
13207 "{:#?}",
13208 scene_delta.new_graph.objects
13209 );
13210
13211 ctx.close().await;
13212 mock_ctx.close().await;
13213 }
13214
13215 #[tokio::test(flavor = "multi_thread")]
13216 async fn test_segments_tangent() {
13217 let initial_source = "\
13218sketch(on = XY) {
13219 line(start = [var 1, var 2], end = [var 3, var 4])
13220 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13221}
13222";
13223
13224 let program = Program::parse(initial_source).unwrap().0.unwrap();
13225
13226 let mut frontend = FrontendState::new();
13227
13228 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13229 let mock_ctx = ExecutorContext::new_mock(None).await;
13230 let version = Version(0);
13231
13232 frontend.hack_set_program(&ctx, program).await.unwrap();
13233 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13234 let sketch_id = sketch_object.id;
13235 let sketch = expect_sketch(sketch_object);
13236 let line1_id = *sketch.segments.get(2).unwrap();
13237 let arc1_id = *sketch.segments.get(6).unwrap();
13238
13239 let constraint = Constraint::Tangent(Tangent {
13240 input: vec![line1_id, arc1_id],
13241 });
13242 let (src_delta, scene_delta) = frontend
13243 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13244 .await
13245 .unwrap();
13246 assert_eq!(
13247 src_delta.text.as_str(),
13248 "\
13249sketch(on = XY) {
13250 line1 = line(start = [var 0.84, var 2.13], end = [var 3.82, var 3.27])
13251 arc1 = arc(start = [var 4.51, var 2.03], end = [var 7.05, var 2.02], center = [var 5.78, var 2.55])
13252 tangent([line1, arc1])
13253}
13254"
13255 );
13256 assert_eq!(
13257 scene_delta.new_graph.objects.len(),
13258 10,
13259 "{:#?}",
13260 scene_delta.new_graph.objects
13261 );
13262
13263 ctx.close().await;
13264 mock_ctx.close().await;
13265 }
13266
13267 #[tokio::test(flavor = "multi_thread")]
13268 async fn test_point_midpoint() {
13269 let initial_source = "\
13270sketch(on = XY) {
13271 point(at = [var 1, var 1])
13272 line(start = [var 0, var 0], end = [var 6, var 4])
13273}
13274";
13275
13276 let program = Program::parse(initial_source).unwrap().0.unwrap();
13277
13278 let mut frontend = FrontendState::new();
13279
13280 let ctx = ExecutorContext::new_mock(None).await;
13281 let version = Version(0);
13282
13283 frontend.program = program.clone();
13284 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13285 frontend.update_state_after_exec(outcome, true);
13286 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13287 let sketch_id = sketch_object.id;
13288 let sketch = expect_sketch(sketch_object);
13289 let point_id = *sketch.segments.first().unwrap();
13290 let line_id = *sketch.segments.get(3).unwrap();
13291
13292 let constraint = Constraint::Midpoint(Midpoint {
13293 point: ConstraintSegment::from(point_id),
13294 segment: line_id,
13295 });
13296 let (src_delta, scene_delta) = frontend
13297 .add_constraint(&ctx, version, sketch_id, constraint)
13298 .await
13299 .unwrap();
13300 assert_eq!(
13301 src_delta.text.as_str(),
13302 "\
13303sketch(on = XY) {
13304 point1 = point(at = [var 2.33, var 1.67])
13305 line1 = line(start = [var -0.67, var -0.33], end = [var 5.33, var 3.67])
13306 midpoint(line1, point = point1)
13307}
13308"
13309 );
13310 assert_eq!(
13311 scene_delta.new_graph.objects.len(),
13312 7,
13313 "{:#?}",
13314 scene_delta.new_graph.objects
13315 );
13316
13317 ctx.close().await;
13318 }
13319
13320 #[tokio::test(flavor = "multi_thread")]
13321 async fn test_segments_symmetric() {
13322 let initial_source = "\
13323sketch(on = XY) {
13324 line(start = [var 0, var 0], end = [var 0, var 4])
13325 line(start = [var 4, var 0], end = [var 4, var 4])
13326 line(start = [var 2, var -1], end = [var 2, var 5])
13327}
13328";
13329
13330 let program = Program::parse(initial_source).unwrap().0.unwrap();
13331
13332 let mut frontend = FrontendState::new();
13333
13334 let ctx = ExecutorContext::new_mock(None).await;
13335 let version = Version(0);
13336
13337 frontend.program = program.clone();
13338 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13339 frontend.update_state_after_exec(outcome, true);
13340 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13341 let sketch_id = sketch_object.id;
13342 let sketch = expect_sketch(sketch_object);
13343 let line1_id = *sketch.segments.get(2).unwrap();
13344 let line2_id = *sketch.segments.get(5).unwrap();
13345 let axis_id = *sketch.segments.get(8).unwrap();
13346
13347 let constraint = Constraint::Symmetric(Symmetric {
13348 input: vec![line1_id, line2_id],
13349 axis: axis_id,
13350 });
13351 let (src_delta, scene_delta) = frontend
13352 .add_constraint(&ctx, version, sketch_id, constraint)
13353 .await
13354 .unwrap();
13355 assert_eq!(
13356 src_delta.text.as_str(),
13357 "\
13358sketch(on = XY) {
13359 line1 = line(start = [var 0, var 0], end = [var 0, var 4])
13360 line2 = line(start = [var 4, var 0], end = [var 4, var 4])
13361 line3 = line(start = [var 2, var -1], end = [var 2, var 5])
13362 symmetric([line1, line2], axis = line3)
13363}
13364"
13365 );
13366 assert_eq!(
13367 scene_delta.new_graph.objects.len(),
13368 12,
13369 "{:#?}",
13370 scene_delta.new_graph.objects
13371 );
13372
13373 ctx.close().await;
13374 }
13375
13376 #[tokio::test(flavor = "multi_thread")]
13377 async fn test_point_arc_midpoint() {
13378 let initial_source = "\
13379sketch(on = XY) {
13380 point(at = [var 6, var 3])
13381 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13382}
13383";
13384
13385 let program = Program::parse(initial_source).unwrap().0.unwrap();
13386
13387 let mut frontend = FrontendState::new();
13388
13389 let ctx = ExecutorContext::new_mock(None).await;
13390 let version = Version(0);
13391
13392 frontend.program = program.clone();
13393 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13394 frontend.update_state_after_exec(outcome, true);
13395 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13396 let sketch_id = sketch_object.id;
13397 let sketch = expect_sketch(sketch_object);
13398 let point_id = *sketch.segments.first().unwrap();
13399 let arc_id = *sketch.segments.get(4).unwrap();
13400
13401 let constraint = Constraint::Midpoint(Midpoint {
13402 point: ConstraintSegment::from(point_id),
13403 segment: arc_id,
13404 });
13405 let (src_delta, scene_delta) = frontend
13406 .add_constraint(&ctx, version, sketch_id, constraint)
13407 .await
13408 .unwrap();
13409 assert_eq!(
13410 src_delta.text.as_str(),
13411 "\
13412sketch(on = XY) {
13413 point1 = point(at = [var 6, var 2.35])
13414 arc1 = arc(start = [var 6, var 2.35], end = [var 6, var 2.35], center = [var 6, var 1.94])
13415 midpoint(arc1, point = point1)
13416}
13417"
13418 );
13419 assert_eq!(
13420 scene_delta.new_graph.objects.len(),
13421 8,
13422 "{:#?}",
13423 scene_delta.new_graph.objects
13424 );
13425
13426 ctx.close().await;
13427 }
13428
13429 #[tokio::test(flavor = "multi_thread")]
13430 async fn test_origin_line_midpoint() {
13431 let initial_source = "\
13432sketch(on = XY) {
13433 line(start = [var 0, var 0], end = [var 6, var 4])
13434}
13435";
13436
13437 let program = Program::parse(initial_source).unwrap().0.unwrap();
13438
13439 let mut frontend = FrontendState::new();
13440
13441 let ctx = ExecutorContext::new_mock(None).await;
13442 let version = Version(0);
13443
13444 frontend.program = program.clone();
13445 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13446 frontend.update_state_after_exec(outcome, true);
13447 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13448 let sketch_id = sketch_object.id;
13449 let sketch = expect_sketch(sketch_object);
13450 let line_id = *sketch.segments.get(2).unwrap();
13451
13452 let constraint = Constraint::Midpoint(Midpoint {
13453 point: ConstraintSegment::ORIGIN,
13454 segment: line_id,
13455 });
13456 let (src_delta, scene_delta) = frontend
13457 .add_constraint(&ctx, version, sketch_id, constraint)
13458 .await
13459 .unwrap();
13460 assert_eq!(
13461 src_delta.text.as_str(),
13462 "\
13463sketch(on = XY) {
13464 line1 = line(start = [var -3, var -2], end = [var 3, var 2])
13465 midpoint(line1, point = ORIGIN)
13466}
13467"
13468 );
13469 assert_eq!(
13470 scene_delta.new_graph.objects.len(),
13471 6,
13472 "{:#?}",
13473 scene_delta.new_graph.objects
13474 );
13475
13476 ctx.close().await;
13477 }
13478
13479 #[tokio::test(flavor = "multi_thread")]
13480 async fn test_origin_arc_midpoint() {
13481 let initial_source = "\
13482sketch(on = XY) {
13483 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13484}
13485";
13486
13487 let program = Program::parse(initial_source).unwrap().0.unwrap();
13488
13489 let mut frontend = FrontendState::new();
13490
13491 let ctx = ExecutorContext::new_mock(None).await;
13492 let version = Version(0);
13493
13494 frontend.program = program.clone();
13495 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13496 frontend.update_state_after_exec(outcome, true);
13497 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13498 let sketch_id = sketch_object.id;
13499 let sketch = expect_sketch(sketch_object);
13500 let arc_id = *sketch.segments.get(3).unwrap();
13501
13502 let constraint = Constraint::Midpoint(Midpoint {
13503 point: ConstraintSegment::ORIGIN,
13504 segment: arc_id,
13505 });
13506 let (src_delta, scene_delta) = frontend
13507 .add_constraint(&ctx, version, sketch_id, constraint)
13508 .await
13509 .unwrap();
13510 assert_eq!(
13511 src_delta.text.as_str(),
13512 "\
13513sketch(on = XY) {
13514 arc1 = arc(start = [var 0.35, var 2.24], end = [var 1.62, var -1.58], center = [var 2.34, var 0.78])
13515 midpoint(arc1, point = ORIGIN)
13516}
13517"
13518 );
13519 assert_eq!(
13520 scene_delta.new_graph.objects.len(),
13521 7,
13522 "{:#?}",
13523 scene_delta.new_graph.objects
13524 );
13525
13526 ctx.close().await;
13527 }
13528
13529 #[tokio::test(flavor = "multi_thread")]
13530 async fn test_segments_symmetric_arcs() {
13531 let initial_source = "\
13532sketch(on = XY) {
13533 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
13534 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
13535 line(start = [var 0, var -10], end = [var 0, var 10])
13536}
13537";
13538
13539 let program = Program::parse(initial_source).unwrap().0.unwrap();
13540
13541 let mut frontend = FrontendState::new();
13542
13543 let ctx = ExecutorContext::new_mock(None).await;
13544 let version = Version(0);
13545
13546 frontend.program = program.clone();
13547 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13548 frontend.update_state_after_exec(outcome, true);
13549 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13550 let sketch_id = sketch_object.id;
13551 let sketch = expect_sketch(sketch_object);
13552 let arc1_id = *sketch.segments.get(3).unwrap();
13553 let arc2_id = *sketch.segments.get(7).unwrap();
13554 let axis_id = *sketch.segments.get(10).unwrap();
13555
13556 let constraint = Constraint::Symmetric(Symmetric {
13557 input: vec![arc1_id, arc2_id],
13558 axis: axis_id,
13559 });
13560 let (src_delta, scene_delta) = frontend
13561 .add_constraint(&ctx, version, sketch_id, constraint)
13562 .await
13563 .unwrap();
13564 assert_eq!(
13565 src_delta.text.as_str(),
13566 "\
13567sketch(on = XY) {
13568 arc1 = arc(start = [var -14.46, var 0], end = [var -10, var 4.65], center = [var -10.14, var 0.31])
13569 arc2 = arc(start = [var 5.49, var 2.26], end = [var 11.58, var -3.47], center = [var 9.34, var 0.25])
13570 line1 = line(start = [var -0.44, var -10], end = [var -0.37, var 10])
13571 symmetric([arc1, arc2], axis = line1)
13572}
13573"
13574 );
13575 assert_eq!(
13576 scene_delta.new_graph.objects.len(),
13577 14,
13578 "{:#?}",
13579 scene_delta.new_graph.objects
13580 );
13581
13582 ctx.close().await;
13583 }
13584
13585 #[tokio::test(flavor = "multi_thread")]
13586 async fn test_sketch_on_face_simple() {
13587 let initial_source = "\
13588len = 2mm
13589cube = startSketchOn(XY)
13590 |> startProfile(at = [0, 0])
13591 |> line(end = [len, 0], tag = $side)
13592 |> line(end = [0, len])
13593 |> line(end = [-len, 0])
13594 |> line(end = [0, -len])
13595 |> close()
13596 |> extrude(length = len)
13597
13598face = faceOf(cube, face = side)
13599";
13600
13601 let program = Program::parse(initial_source).unwrap().0.unwrap();
13602
13603 let mut frontend = FrontendState::new();
13604
13605 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13606 let mock_ctx = ExecutorContext::new_mock(None).await;
13607 let version = Version(0);
13608
13609 frontend.hack_set_program(&ctx, program).await.unwrap();
13610 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
13611 let face_id = face_object.id;
13612
13613 let sketch_args = SketchCtor {
13614 on: Plane::Object(face_id),
13615 };
13616 let (_src_delta, scene_delta, sketch_id) = frontend
13617 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13618 .await
13619 .unwrap();
13620 assert_eq!(sketch_id, ObjectId(2));
13621 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13622 let sketch_object = &scene_delta.new_graph.objects[2];
13623 assert_eq!(sketch_object.id, ObjectId(2));
13624 assert_eq!(
13625 sketch_object.kind,
13626 ObjectKind::Sketch(Sketch {
13627 args: SketchCtor {
13628 on: Plane::Object(face_id),
13629 },
13630 plane: face_id,
13631 segments: vec![],
13632 constraints: vec![],
13633 })
13634 );
13635 assert_eq!(scene_delta.new_graph.objects.len(), 8);
13636
13637 ctx.close().await;
13638 mock_ctx.close().await;
13639 }
13640
13641 #[tokio::test(flavor = "multi_thread")]
13642 async fn test_sketch_on_wall_artifact_from_region_extrude() {
13643 let initial_source = "\
13644s = sketch(on = YZ) {
13645 line1 = line(start = [0, 0], end = [0, 1])
13646 line2 = line(start = [0, 1], end = [1, 1])
13647 line3 = line(start = [1, 1], end = [0, 0])
13648}
13649region001 = region(point = [0.1, 0.1], sketch = s)
13650extrude001 = extrude(region001, length = 5)
13651";
13652
13653 let program = Program::parse(initial_source).unwrap().0.unwrap();
13654
13655 let mut frontend = FrontendState::new();
13656 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13657 let version = Version(0);
13658
13659 frontend.hack_set_program(&ctx, program).await.unwrap();
13660 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13661
13662 let sketch_args = SketchCtor {
13663 on: Plane::Object(wall_object_id),
13664 };
13665 let (src_delta, _scene_delta, _sketch_id) = frontend
13666 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13667 .await
13668 .unwrap();
13669 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13670
13671 ctx.close().await;
13672 }
13673
13674 #[tokio::test(flavor = "multi_thread")]
13675 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
13676 let initial_source = "\
13677sketch001 = sketch(on = YZ) {
13678 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
13679 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
13680 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
13681 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
13682 coincident([line1.end, line2.start])
13683 coincident([line2.end, line3.start])
13684 coincident([line3.end, line4.start])
13685 coincident([line4.end, line1.start])
13686 parallel([line2, line4])
13687 parallel([line3, line1])
13688 perpendicular([line1, line2])
13689 horizontal(line3)
13690 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
13691}
13692region001 = region(point = [3.1, 3.74], sketch = sketch001)
13693extrude001 = extrude(region001, length = 5)
13694";
13695
13696 let program = Program::parse(initial_source).unwrap().0.unwrap();
13697
13698 let mut frontend = FrontendState::new();
13699 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13700 let version = Version(0);
13701
13702 frontend.hack_set_program(&ctx, program).await.unwrap();
13703 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13704
13705 let sketch_args = SketchCtor {
13706 on: Plane::Object(wall_object_id),
13707 };
13708 let (src_delta, _scene_delta, _sketch_id) = frontend
13709 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13710 .await
13711 .unwrap();
13712 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13713
13714 ctx.close().await;
13715 }
13716
13717 #[tokio::test(flavor = "multi_thread")]
13718 async fn test_sketch_on_plane_incremental() {
13719 let initial_source = "\
13720len = 2mm
13721cube = startSketchOn(XY)
13722 |> startProfile(at = [0, 0])
13723 |> line(end = [len, 0], tag = $side)
13724 |> line(end = [0, len])
13725 |> line(end = [-len, 0])
13726 |> line(end = [0, -len])
13727 |> close()
13728 |> extrude(length = len)
13729
13730plane = planeOf(cube, face = side)
13731";
13732
13733 let program = Program::parse(initial_source).unwrap().0.unwrap();
13734
13735 let mut frontend = FrontendState::new();
13736
13737 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13738 let mock_ctx = ExecutorContext::new_mock(None).await;
13739 let version = Version(0);
13740
13741 frontend.hack_set_program(&ctx, program).await.unwrap();
13742 let plane_object = frontend
13744 .scene_graph
13745 .objects
13746 .iter()
13747 .rev()
13748 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
13749 .unwrap();
13750 let plane_id = plane_object.id;
13751
13752 let sketch_args = SketchCtor {
13753 on: Plane::Object(plane_id),
13754 };
13755 let (src_delta, scene_delta, sketch_id) = frontend
13756 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13757 .await
13758 .unwrap();
13759 assert_eq!(
13760 src_delta.text.as_str(),
13761 "\
13762len = 2mm
13763cube = startSketchOn(XY)
13764 |> startProfile(at = [0, 0])
13765 |> line(end = [len, 0], tag = $side)
13766 |> line(end = [0, len])
13767 |> line(end = [-len, 0])
13768 |> line(end = [0, -len])
13769 |> close()
13770 |> extrude(length = len)
13771
13772plane = planeOf(cube, face = side)
13773sketch001 = sketch(on = plane) {
13774}
13775"
13776 );
13777 assert_eq!(sketch_id, ObjectId(2));
13778 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13779 let sketch_object = &scene_delta.new_graph.objects[2];
13780 assert_eq!(sketch_object.id, ObjectId(2));
13781 assert_eq!(
13782 sketch_object.kind,
13783 ObjectKind::Sketch(Sketch {
13784 args: SketchCtor {
13785 on: Plane::Object(plane_id),
13786 },
13787 plane: plane_id,
13788 segments: vec![],
13789 constraints: vec![],
13790 })
13791 );
13792 assert_eq!(scene_delta.new_graph.objects.len(), 9);
13793
13794 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
13795 assert_eq!(plane_object.id, plane_id);
13796 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
13797
13798 ctx.close().await;
13799 mock_ctx.close().await;
13800 }
13801
13802 #[tokio::test(flavor = "multi_thread")]
13803 async fn test_new_sketch_uses_unique_variable_name() {
13804 let initial_source = "\
13805sketch1 = sketch(on = XY) {
13806}
13807";
13808
13809 let program = Program::parse(initial_source).unwrap().0.unwrap();
13810
13811 let mut frontend = FrontendState::new();
13812 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13813 let version = Version(0);
13814
13815 frontend.hack_set_program(&ctx, program).await.unwrap();
13816
13817 let sketch_args = SketchCtor {
13818 on: Plane::Default(PlaneName::Yz),
13819 };
13820 let (src_delta, _, _) = frontend
13821 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13822 .await
13823 .unwrap();
13824
13825 assert_eq!(
13826 src_delta.text.as_str(),
13827 "\
13828sketch1 = sketch(on = XY) {
13829}
13830sketch001 = sketch(on = YZ) {
13831}
13832"
13833 );
13834
13835 ctx.close().await;
13836 }
13837
13838 #[tokio::test(flavor = "multi_thread")]
13839 async fn test_new_sketch_twice_using_same_plane() {
13840 let initial_source = "\
13841sketch1 = sketch(on = XY) {
13842}
13843";
13844
13845 let program = Program::parse(initial_source).unwrap().0.unwrap();
13846
13847 let mut frontend = FrontendState::new();
13848 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13849 let version = Version(0);
13850
13851 frontend.hack_set_program(&ctx, program).await.unwrap();
13852
13853 let sketch_args = SketchCtor {
13854 on: Plane::Default(PlaneName::Xy),
13855 };
13856 let (src_delta, _, _) = frontend
13857 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13858 .await
13859 .unwrap();
13860
13861 assert_eq!(
13862 src_delta.text.as_str(),
13863 "\
13864sketch1 = sketch(on = XY) {
13865}
13866sketch001 = sketch(on = XY) {
13867}
13868"
13869 );
13870
13871 ctx.close().await;
13872 }
13873
13874 #[tokio::test(flavor = "multi_thread")]
13875 async fn test_sketch_mode_reuses_cached_on_expression() {
13876 let initial_source = "\
13877width = 2mm
13878sketch(on = offsetPlane(XY, offset = width)) {
13879 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
13880 distance([line1.start, line1.end]) == width
13881}
13882";
13883 let program = Program::parse(initial_source).unwrap().0.unwrap();
13884
13885 let mut frontend = FrontendState::new();
13886 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13887 let mock_ctx = ExecutorContext::new_mock(None).await;
13888 let version = Version(0);
13889 let project_id = ProjectId(0);
13890 let file_id = FileId(0);
13891
13892 frontend.hack_set_program(&ctx, program).await.unwrap();
13893 let initial_object_count = frontend.scene_graph.objects.len();
13894 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
13895 .expect("Expected sketch object to exist")
13896 .id;
13897
13898 let scene_delta = frontend
13901 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
13902 .await
13903 .unwrap();
13904 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
13905
13906 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
13909 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
13910
13911 ctx.close().await;
13912 mock_ctx.close().await;
13913 }
13914
13915 #[tokio::test(flavor = "multi_thread")]
13916 async fn test_multiple_sketch_blocks() {
13917 let initial_source = "\
13918// Cube that requires the engine.
13919width = 2
13920sketch001 = startSketchOn(XY)
13921profile001 = startProfile(sketch001, at = [0, 0])
13922 |> yLine(length = width, tag = $seg1)
13923 |> xLine(length = width)
13924 |> yLine(length = -width)
13925 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
13926 |> close()
13927extrude001 = extrude(profile001, length = width)
13928
13929// Get a value that requires the engine.
13930x = segLen(seg1)
13931
13932// Triangle with side length 2*x.
13933sketch(on = XY) {
13934 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
13935 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
13936 coincident([line1.end, line2.start])
13937 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
13938 coincident([line2.end, line3.start])
13939 coincident([line3.end, line1.start])
13940 equalLength([line3, line1])
13941 equalLength([line1, line2])
13942 distance([line1.start, line1.end]) == 2*x
13943}
13944
13945// Line segment with length x.
13946sketch2 = sketch(on = XY) {
13947 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
13948 distance([line1.start, line1.end]) == x
13949}
13950";
13951
13952 let program = Program::parse(initial_source).unwrap().0.unwrap();
13953
13954 let mut frontend = FrontendState::new();
13955
13956 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13957 let mock_ctx = ExecutorContext::new_mock(None).await;
13958 let version = Version(0);
13959 let project_id = ProjectId(0);
13960 let file_id = FileId(0);
13961
13962 frontend.hack_set_program(&ctx, program).await.unwrap();
13963 let sketch_objects = frontend
13964 .scene_graph
13965 .objects
13966 .iter()
13967 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
13968 .collect::<Vec<_>>();
13969 let sketch1_id = sketch_objects.first().unwrap().id;
13970 let sketch2_id = sketch_objects.get(1).unwrap().id;
13971 let point1_id = ObjectId(sketch1_id.0 + 1);
13973 let point2_id = ObjectId(sketch2_id.0 + 1);
13975
13976 let scene_delta = frontend
13985 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
13986 .await
13987 .unwrap();
13988 assert_eq!(
13989 scene_delta.new_graph.objects.len(),
13990 18,
13991 "{:#?}",
13992 scene_delta.new_graph.objects
13993 );
13994
13995 let point_ctor = PointCtor {
13997 position: Point2d {
13998 x: Expr::Var(Number {
13999 value: 1.0,
14000 units: NumericSuffix::Mm,
14001 }),
14002 y: Expr::Var(Number {
14003 value: 2.0,
14004 units: NumericSuffix::Mm,
14005 }),
14006 },
14007 };
14008 let segments = vec![ExistingSegmentCtor {
14009 id: point1_id,
14010 ctor: SegmentCtor::Point(point_ctor),
14011 }];
14012 let (src_delta, _) = frontend
14013 .edit_segments(&mock_ctx, version, sketch1_id, segments)
14014 .await
14015 .unwrap();
14016 assert_eq!(
14018 src_delta.text.as_str(),
14019 "\
14020// Cube that requires the engine.
14021width = 2
14022sketch001 = startSketchOn(XY)
14023profile001 = startProfile(sketch001, at = [0, 0])
14024 |> yLine(length = width, tag = $seg1)
14025 |> xLine(length = width)
14026 |> yLine(length = -width)
14027 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14028 |> close()
14029extrude001 = extrude(profile001, length = width)
14030
14031// Get a value that requires the engine.
14032x = segLen(seg1)
14033
14034// Triangle with side length 2*x.
14035sketch(on = XY) {
14036 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14037 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14038 coincident([line1.end, line2.start])
14039 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14040 coincident([line2.end, line3.start])
14041 coincident([line3.end, line1.start])
14042 equalLength([line3, line1])
14043 equalLength([line1, line2])
14044 distance([line1.start, line1.end]) == 2 * x
14045}
14046
14047// Line segment with length x.
14048sketch2 = sketch(on = XY) {
14049 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14050 distance([line1.start, line1.end]) == x
14051}
14052"
14053 );
14054 let edited_sketch1_source = src_delta.text.clone();
14055
14056 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
14058 assert_eq!(src_delta.text, edited_sketch1_source);
14059 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14067 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
14068
14069 let scene_delta = frontend
14077 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14078 .await
14079 .unwrap();
14080 assert_eq!(
14081 scene_delta.new_graph.objects.len(),
14082 24,
14083 "{:#?}",
14084 scene_delta.new_graph.objects
14085 );
14086
14087 let point_ctor = PointCtor {
14089 position: Point2d {
14090 x: Expr::Var(Number {
14091 value: 3.0,
14092 units: NumericSuffix::Mm,
14093 }),
14094 y: Expr::Var(Number {
14095 value: 4.0,
14096 units: NumericSuffix::Mm,
14097 }),
14098 },
14099 };
14100 let segments = vec![ExistingSegmentCtor {
14101 id: point2_id,
14102 ctor: SegmentCtor::Point(point_ctor),
14103 }];
14104 let (src_delta, _) = frontend
14105 .edit_segments(&mock_ctx, version, sketch2_id, segments)
14106 .await
14107 .unwrap();
14108 assert_eq!(
14110 src_delta.text.as_str(),
14111 "\
14112// Cube that requires the engine.
14113width = 2
14114sketch001 = startSketchOn(XY)
14115profile001 = startProfile(sketch001, at = [0, 0])
14116 |> yLine(length = width, tag = $seg1)
14117 |> xLine(length = width)
14118 |> yLine(length = -width)
14119 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14120 |> close()
14121extrude001 = extrude(profile001, length = width)
14122
14123// Get a value that requires the engine.
14124x = segLen(seg1)
14125
14126// Triangle with side length 2*x.
14127sketch(on = XY) {
14128 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14129 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14130 coincident([line1.end, line2.start])
14131 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14132 coincident([line2.end, line3.start])
14133 coincident([line3.end, line1.start])
14134 equalLength([line3, line1])
14135 equalLength([line1, line2])
14136 distance([line1.start, line1.end]) == 2 * x
14137}
14138
14139// Line segment with length x.
14140sketch2 = sketch(on = XY) {
14141 line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
14142 distance([line1.start, line1.end]) == x
14143}
14144"
14145 );
14146 let edited_sketch2_source = src_delta.text.clone();
14147
14148 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
14150 assert_eq!(src_delta.text, edited_sketch2_source);
14151
14152 ctx.close().await;
14153 mock_ctx.close().await;
14154 }
14155
14156 #[tokio::test(flavor = "multi_thread")]
14157 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
14158 clear_mem_cache().await;
14159
14160 let source = r#"sketch001 = sketch(on = XZ) {
14161 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
14162}
14163sketch002 = sketch(on = XY) {
14164 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
14165 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
14166 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
14167 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
14168 coincident([line1.end, line2.start])
14169 coincident([line2.end, line3.start])
14170 coincident([line3.end, line4.start])
14171 coincident([line4.end, line1.start])
14172 parallel([line2, line4])
14173 parallel([line3, line1])
14174 perpendicular([line1, line2])
14175 horizontal(line3)
14176 coincident([line1.start, ORIGIN])
14177}
14178"#;
14179
14180 let program = Program::parse(source).unwrap().0.unwrap();
14181 let mut frontend = FrontendState::new();
14182 let ctx = ExecutorContext::new_with_engine(
14183 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
14184 Default::default(),
14185 );
14186 let mock_ctx = ExecutorContext::new_mock(None).await;
14187 let version = Version(0);
14188 let project_id = ProjectId(0);
14189 let file_id = FileId(0);
14190
14191 frontend.hack_set_program(&ctx, program).await.unwrap();
14192 let sketch_objects = frontend
14193 .scene_graph
14194 .objects
14195 .iter()
14196 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
14197 .collect::<Vec<_>>();
14198 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
14199
14200 let sketch1_id = sketch_objects[0].id;
14201 let sketch2_id = sketch_objects[1].id;
14202
14203 frontend
14204 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14205 .await
14206 .unwrap();
14207 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14208
14209 let scene_delta = frontend
14210 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14211 .await
14212 .unwrap();
14213 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
14214
14215 clear_mem_cache().await;
14216 ctx.close().await;
14217 mock_ctx.close().await;
14218 }
14219
14220 #[tokio::test(flavor = "multi_thread")]
14225 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
14226 let initial_source = "@settings(defaultLengthUnit = mm)
14228
14229
14230
14231sketch001 = sketch(on = XY) {
14232 point(at = [1in, 2in])
14233}
14234";
14235
14236 let program = Program::parse(initial_source).unwrap().0.unwrap();
14237 let mut frontend = FrontendState::new();
14238
14239 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14240 let mock_ctx = ExecutorContext::new_mock(None).await;
14241 let version = Version(0);
14242 let project_id = ProjectId(0);
14243 let file_id = FileId(0);
14244
14245 frontend.hack_set_program(&ctx, program).await.unwrap();
14246 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14247 let sketch_id = sketch_object.id;
14248
14249 frontend
14251 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14252 .await
14253 .unwrap();
14254
14255 let point_ctor = PointCtor {
14257 position: Point2d {
14258 x: Expr::Number(Number {
14259 value: 5.0,
14260 units: NumericSuffix::Mm,
14261 }),
14262 y: Expr::Number(Number {
14263 value: 6.0,
14264 units: NumericSuffix::Mm,
14265 }),
14266 },
14267 };
14268 let segment = SegmentCtor::Point(point_ctor);
14269 let (src_delta, scene_delta) = frontend
14270 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14271 .await
14272 .unwrap();
14273 assert!(
14275 src_delta.text.contains("point(at = [5mm, 6mm])"),
14276 "Expected new point in source, got: {}",
14277 src_delta.text
14278 );
14279 assert!(!scene_delta.new_objects.is_empty());
14280
14281 ctx.close().await;
14282 mock_ctx.close().await;
14283 }
14284
14285 #[tokio::test(flavor = "multi_thread")]
14286 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
14287 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
14288
14289 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
14290 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
14291
14292 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
14293 assert!(
14294 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
14295 "Expected experimental settings to be added to source"
14296 );
14297 }
14298
14299 #[tokio::test(flavor = "multi_thread")]
14300 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
14301 let initial_source = "@settings(defaultLengthUnit = mm)
14303
14304
14305
14306s = sketch(on = XY) {}
14307";
14308
14309 let program = Program::parse(initial_source).unwrap().0.unwrap();
14310 let mut frontend = FrontendState::new();
14311
14312 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14313 let mock_ctx = ExecutorContext::new_mock(None).await;
14314 let version = Version(0);
14315
14316 frontend.hack_set_program(&ctx, program).await.unwrap();
14317 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14318 let sketch_id = sketch_object.id;
14319
14320 let line_ctor = LineCtor {
14321 start: Point2d {
14322 x: Expr::Number(Number {
14323 value: 0.0,
14324 units: NumericSuffix::Mm,
14325 }),
14326 y: Expr::Number(Number {
14327 value: 0.0,
14328 units: NumericSuffix::Mm,
14329 }),
14330 },
14331 end: Point2d {
14332 x: Expr::Number(Number {
14333 value: 10.0,
14334 units: NumericSuffix::Mm,
14335 }),
14336 y: Expr::Number(Number {
14337 value: 10.0,
14338 units: NumericSuffix::Mm,
14339 }),
14340 },
14341 construction: None,
14342 };
14343 let segment = SegmentCtor::Line(line_ctor);
14344 let (src_delta, scene_delta) = frontend
14345 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14346 .await
14347 .unwrap();
14348 assert!(
14349 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14350 "Expected line in source, got: {}",
14351 src_delta.text
14352 );
14353 assert_eq!(scene_delta.new_objects.len(), 3);
14355
14356 ctx.close().await;
14357 mock_ctx.close().await;
14358 }
14359
14360 #[tokio::test(flavor = "multi_thread")]
14361 async fn test_extra_newlines_between_operations_edit_line() {
14362 let initial_source = "@settings(defaultLengthUnit = mm)
14364
14365
14366sketch001 = sketch(on = XY) {
14367
14368 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14369
14370}
14371";
14372
14373 let program = Program::parse(initial_source).unwrap().0.unwrap();
14374 let mut frontend = FrontendState::new();
14375
14376 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14377 let mock_ctx = ExecutorContext::new_mock(None).await;
14378 let version = Version(0);
14379 let project_id = ProjectId(0);
14380 let file_id = FileId(0);
14381
14382 frontend.hack_set_program(&ctx, program).await.unwrap();
14383 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14384 let sketch_id = sketch_object.id;
14385 let sketch = expect_sketch(sketch_object);
14386
14387 let line_id = sketch
14389 .segments
14390 .iter()
14391 .copied()
14392 .find(|seg_id| {
14393 matches!(
14394 &frontend.scene_graph.objects[seg_id.0].kind,
14395 ObjectKind::Segment {
14396 segment: Segment::Line(_)
14397 }
14398 )
14399 })
14400 .expect("Expected a line segment in sketch");
14401
14402 frontend
14404 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14405 .await
14406 .unwrap();
14407
14408 let line_ctor = LineCtor {
14410 start: Point2d {
14411 x: Expr::Var(Number {
14412 value: 1.0,
14413 units: NumericSuffix::Mm,
14414 }),
14415 y: Expr::Var(Number {
14416 value: 2.0,
14417 units: NumericSuffix::Mm,
14418 }),
14419 },
14420 end: Point2d {
14421 x: Expr::Var(Number {
14422 value: 13.0,
14423 units: NumericSuffix::Mm,
14424 }),
14425 y: Expr::Var(Number {
14426 value: 14.0,
14427 units: NumericSuffix::Mm,
14428 }),
14429 },
14430 construction: None,
14431 };
14432 let segments = vec![ExistingSegmentCtor {
14433 id: line_id,
14434 ctor: SegmentCtor::Line(line_ctor),
14435 }];
14436 let (src_delta, _scene_delta) = frontend
14437 .edit_segments(&mock_ctx, version, sketch_id, segments)
14438 .await
14439 .unwrap();
14440 assert!(
14441 src_delta
14442 .text
14443 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
14444 "Expected edited line in source, got: {}",
14445 src_delta.text
14446 );
14447
14448 ctx.close().await;
14449 mock_ctx.close().await;
14450 }
14451
14452 #[tokio::test(flavor = "multi_thread")]
14453 async fn test_extra_newlines_delete_segment() {
14454 let initial_source = "@settings(defaultLengthUnit = mm)
14456
14457
14458
14459sketch001 = sketch(on = XY) {
14460 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14461}
14462";
14463
14464 let program = Program::parse(initial_source).unwrap().0.unwrap();
14465 let mut frontend = FrontendState::new();
14466
14467 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14468 let mock_ctx = ExecutorContext::new_mock(None).await;
14469 let version = Version(0);
14470
14471 frontend.hack_set_program(&ctx, program).await.unwrap();
14472 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14473 let sketch_id = sketch_object.id;
14474 let sketch = expect_sketch(sketch_object);
14475
14476 assert_eq!(sketch.segments.len(), 3);
14478 let circle_id = sketch.segments[2];
14479
14480 let (src_delta, scene_delta) = frontend
14482 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
14483 .await
14484 .unwrap();
14485 assert!(
14486 src_delta.text.contains("sketch(on = XY) {"),
14487 "Expected sketch block in source, got: {}",
14488 src_delta.text
14489 );
14490 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
14491 let new_sketch = expect_sketch(new_sketch_object);
14492 assert_eq!(new_sketch.segments.len(), 0);
14493
14494 ctx.close().await;
14495 mock_ctx.close().await;
14496 }
14497
14498 #[tokio::test(flavor = "multi_thread")]
14499 async fn test_unformatted_source_add_arc() {
14500 let initial_source = "@settings(defaultLengthUnit = mm)
14502
14503
14504
14505
14506sketch001 = sketch(on = XY) {
14507}
14508";
14509
14510 let program = Program::parse(initial_source).unwrap().0.unwrap();
14511 let mut frontend = FrontendState::new();
14512
14513 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14514 let mock_ctx = ExecutorContext::new_mock(None).await;
14515 let version = Version(0);
14516
14517 frontend.hack_set_program(&ctx, program).await.unwrap();
14518 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14519 let sketch_id = sketch_object.id;
14520
14521 let arc_ctor = ArcCtor {
14522 start: Point2d {
14523 x: Expr::Var(Number {
14524 value: 5.0,
14525 units: NumericSuffix::Mm,
14526 }),
14527 y: Expr::Var(Number {
14528 value: 0.0,
14529 units: NumericSuffix::Mm,
14530 }),
14531 },
14532 end: Point2d {
14533 x: Expr::Var(Number {
14534 value: 0.0,
14535 units: NumericSuffix::Mm,
14536 }),
14537 y: Expr::Var(Number {
14538 value: 5.0,
14539 units: NumericSuffix::Mm,
14540 }),
14541 },
14542 center: Point2d {
14543 x: Expr::Var(Number {
14544 value: 0.0,
14545 units: NumericSuffix::Mm,
14546 }),
14547 y: Expr::Var(Number {
14548 value: 0.0,
14549 units: NumericSuffix::Mm,
14550 }),
14551 },
14552 construction: None,
14553 };
14554 let segment = SegmentCtor::Arc(arc_ctor);
14555 let (src_delta, scene_delta) = frontend
14556 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14557 .await
14558 .unwrap();
14559 assert!(
14560 src_delta
14561 .text
14562 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
14563 "Expected arc in source, got: {}",
14564 src_delta.text
14565 );
14566 assert!(!scene_delta.new_objects.is_empty());
14567
14568 ctx.close().await;
14569 mock_ctx.close().await;
14570 }
14571
14572 #[tokio::test(flavor = "multi_thread")]
14573 async fn test_extra_newlines_add_circle() {
14574 let initial_source = "@settings(defaultLengthUnit = mm)
14576
14577
14578
14579sketch001 = sketch(on = XY) {
14580}
14581";
14582
14583 let program = Program::parse(initial_source).unwrap().0.unwrap();
14584 let mut frontend = FrontendState::new();
14585
14586 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14587 let mock_ctx = ExecutorContext::new_mock(None).await;
14588 let version = Version(0);
14589
14590 frontend.hack_set_program(&ctx, program).await.unwrap();
14591 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14592 let sketch_id = sketch_object.id;
14593
14594 let circle_ctor = CircleCtor {
14595 start: Point2d {
14596 x: Expr::Var(Number {
14597 value: 5.0,
14598 units: NumericSuffix::Mm,
14599 }),
14600 y: Expr::Var(Number {
14601 value: 0.0,
14602 units: NumericSuffix::Mm,
14603 }),
14604 },
14605 center: Point2d {
14606 x: Expr::Var(Number {
14607 value: 0.0,
14608 units: NumericSuffix::Mm,
14609 }),
14610 y: Expr::Var(Number {
14611 value: 0.0,
14612 units: NumericSuffix::Mm,
14613 }),
14614 },
14615 construction: None,
14616 };
14617 let segment = SegmentCtor::Circle(circle_ctor);
14618 let (src_delta, scene_delta) = frontend
14619 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14620 .await
14621 .unwrap();
14622 assert!(
14623 src_delta
14624 .text
14625 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
14626 "Expected circle in source, got: {}",
14627 src_delta.text
14628 );
14629 assert!(!scene_delta.new_objects.is_empty());
14630
14631 ctx.close().await;
14632 mock_ctx.close().await;
14633 }
14634
14635 #[tokio::test(flavor = "multi_thread")]
14636 async fn test_extra_newlines_add_constraint() {
14637 let initial_source = "@settings(defaultLengthUnit = mm)
14639
14640
14641
14642sketch001 = sketch(on = XY) {
14643 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14644 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
14645}
14646";
14647
14648 let program = Program::parse(initial_source).unwrap().0.unwrap();
14649 let mut frontend = FrontendState::new();
14650
14651 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14652 let mock_ctx = ExecutorContext::new_mock(None).await;
14653 let version = Version(0);
14654 let project_id = ProjectId(0);
14655 let file_id = FileId(0);
14656
14657 frontend.hack_set_program(&ctx, program).await.unwrap();
14658 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14659 let sketch_id = sketch_object.id;
14660 let sketch = expect_sketch(sketch_object);
14661
14662 let line_ids: Vec<ObjectId> = sketch
14664 .segments
14665 .iter()
14666 .copied()
14667 .filter(|seg_id| {
14668 matches!(
14669 &frontend.scene_graph.objects[seg_id.0].kind,
14670 ObjectKind::Segment {
14671 segment: Segment::Line(_)
14672 }
14673 )
14674 })
14675 .collect();
14676 assert_eq!(line_ids.len(), 2, "Expected two line segments");
14677
14678 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
14679 let ObjectKind::Segment {
14680 segment: Segment::Line(line1_data),
14681 } = &line1.kind
14682 else {
14683 panic!("Expected line");
14684 };
14685 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
14686 let ObjectKind::Segment {
14687 segment: Segment::Line(line2_data),
14688 } = &line2.kind
14689 else {
14690 panic!("Expected line");
14691 };
14692
14693 let constraint = Constraint::Coincident(Coincident {
14695 segments: vec![line1_data.end.into(), line2_data.start.into()],
14696 });
14697
14698 frontend
14700 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14701 .await
14702 .unwrap();
14703 let (src_delta, _scene_delta) = frontend
14704 .add_constraint(&mock_ctx, version, sketch_id, constraint)
14705 .await
14706 .unwrap();
14707 assert!(
14708 src_delta.text.contains("coincident("),
14709 "Expected coincident constraint in source, got: {}",
14710 src_delta.text
14711 );
14712
14713 ctx.close().await;
14714 mock_ctx.close().await;
14715 }
14716
14717 #[tokio::test(flavor = "multi_thread")]
14718 async fn test_extra_newlines_add_line_then_edit_line() {
14719 let initial_source = "@settings(defaultLengthUnit = mm)
14721
14722
14723
14724sketch001 = sketch(on = XY) {
14725}
14726";
14727
14728 let program = Program::parse(initial_source).unwrap().0.unwrap();
14729 let mut frontend = FrontendState::new();
14730
14731 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14732 let mock_ctx = ExecutorContext::new_mock(None).await;
14733 let version = Version(0);
14734
14735 frontend.hack_set_program(&ctx, program).await.unwrap();
14736 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14737 let sketch_id = sketch_object.id;
14738
14739 let line_ctor = LineCtor {
14741 start: Point2d {
14742 x: Expr::Number(Number {
14743 value: 0.0,
14744 units: NumericSuffix::Mm,
14745 }),
14746 y: Expr::Number(Number {
14747 value: 0.0,
14748 units: NumericSuffix::Mm,
14749 }),
14750 },
14751 end: Point2d {
14752 x: Expr::Number(Number {
14753 value: 10.0,
14754 units: NumericSuffix::Mm,
14755 }),
14756 y: Expr::Number(Number {
14757 value: 10.0,
14758 units: NumericSuffix::Mm,
14759 }),
14760 },
14761 construction: None,
14762 };
14763 let segment = SegmentCtor::Line(line_ctor);
14764 let (src_delta, scene_delta) = frontend
14765 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14766 .await
14767 .unwrap();
14768 assert!(
14769 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14770 "Expected line in source after add, got: {}",
14771 src_delta.text
14772 );
14773 let line_id = *scene_delta.new_objects.last().unwrap();
14775
14776 let line_ctor = LineCtor {
14778 start: Point2d {
14779 x: Expr::Number(Number {
14780 value: 1.0,
14781 units: NumericSuffix::Mm,
14782 }),
14783 y: Expr::Number(Number {
14784 value: 2.0,
14785 units: NumericSuffix::Mm,
14786 }),
14787 },
14788 end: Point2d {
14789 x: Expr::Number(Number {
14790 value: 13.0,
14791 units: NumericSuffix::Mm,
14792 }),
14793 y: Expr::Number(Number {
14794 value: 14.0,
14795 units: NumericSuffix::Mm,
14796 }),
14797 },
14798 construction: None,
14799 };
14800 let segments = vec![ExistingSegmentCtor {
14801 id: line_id,
14802 ctor: SegmentCtor::Line(line_ctor),
14803 }];
14804 let (src_delta, scene_delta) = frontend
14805 .edit_segments(&mock_ctx, version, sketch_id, segments)
14806 .await
14807 .unwrap();
14808 assert!(
14809 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
14810 "Expected edited line in source, got: {}",
14811 src_delta.text
14812 );
14813 assert_eq!(scene_delta.new_objects, vec![]);
14814
14815 ctx.close().await;
14816 mock_ctx.close().await;
14817 }
14818}