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 #[cfg(not(feature = "artifact-graph"))]
2560 let _ = spline_node_ref;
2561
2562 self.program = new_program.clone();
2563
2564 let mut truncated_program = new_program;
2565 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2566 .map_err(KclErrorWithOutputs::no_outputs)?;
2567
2568 let outcome = ctx
2569 .run_mock(
2570 &truncated_program,
2571 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2572 )
2573 .await?;
2574
2575 #[cfg(not(feature = "artifact-graph"))]
2576 let new_object_ids = Vec::new();
2577 #[cfg(feature = "artifact-graph")]
2578 let new_object_ids = {
2579 let make_err =
2580 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2581 let segment_id = outcome
2582 .source_range_to_object
2583 .get(&spline_node_ref.range)
2584 .copied()
2585 .ok_or_else(|| {
2586 make_err(format!(
2587 "Source range of controlPointSpline not found: {spline_node_ref:?}"
2588 ))
2589 })?;
2590 let segment_object = outcome
2591 .scene_objects
2592 .get(segment_id.0)
2593 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2594 let ObjectKind::Segment { segment } = &segment_object.kind else {
2595 return Err(make_err(format!(
2596 "Object is not a segment, it is {}",
2597 segment_object.kind.human_friendly_kind_with_article()
2598 )));
2599 };
2600 let Segment::ControlPointSpline(spline) = segment else {
2601 return Err(make_err(format!(
2602 "Segment is not a control point spline, it is {}",
2603 segment.human_friendly_kind_with_article()
2604 )));
2605 };
2606
2607 let mut ids = outcome
2608 .scene_objects
2609 .iter()
2610 .filter_map(|obj| match &obj.kind {
2611 ObjectKind::Segment {
2612 segment: Segment::Line(line),
2613 } if line.owner == Some(segment_id) => Some(obj.id),
2614 _ => None,
2615 })
2616 .collect::<Vec<_>>();
2617 ids.extend(spline.controls.clone());
2618 ids.push(segment_id);
2619 ids
2620 };
2621 let src_delta = SourceDelta { text: new_source };
2622 let outcome = self.update_state_after_exec(outcome, false);
2623 let scene_graph_delta = SceneGraphDelta {
2624 new_graph: self.scene_graph_for_ui(),
2625 invalidates_ids: false,
2626 new_objects: new_object_ids,
2627 exec_outcome: outcome,
2628 };
2629 Ok((src_delta, scene_graph_delta))
2630 }
2631
2632 fn edit_point(
2633 &mut self,
2634 new_ast: &mut ast::Node<ast::Program>,
2635 sketch: ObjectId,
2636 point: ObjectId,
2637 ctor: PointCtor,
2638 ) -> Result<(), KclError> {
2639 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2641
2642 let sketch_id = sketch;
2644 let sketch_object = self
2645 .scene_graph
2646 .objects
2647 .get(sketch_id.0)
2648 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2649 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2650 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2651 };
2652 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2653 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2654 })?;
2655 let point_id = point;
2657 let point_object = self
2658 .scene_graph
2659 .objects
2660 .get(point_id.0)
2661 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2662 let ObjectKind::Segment {
2663 segment: Segment::Point(point),
2664 } = &point_object.kind
2665 else {
2666 return Err(KclError::refactor(format!(
2667 "Object is not a point segment: {point_object:?}"
2668 )));
2669 };
2670
2671 if let Some(owner_id) = point.owner {
2673 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2674 KclError::refactor(format!(
2675 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2676 ))
2677 })?;
2678 let ObjectKind::Segment { segment } = &owner_object.kind else {
2679 return Err(KclError::refactor(format!(
2680 "Internal: Owner of point is not a segment, but found {}",
2681 owner_object.kind.human_friendly_kind_with_article()
2682 )));
2683 };
2684
2685 if let Segment::Line(line) = segment {
2687 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2688 return Err(KclError::refactor(format!(
2689 "Internal: Owner of point does not have line ctor, but found {}",
2690 line.ctor.human_friendly_kind_with_article()
2691 )));
2692 };
2693 let mut line_ctor = line_ctor.clone();
2694 if line.start == point_id {
2696 line_ctor.start = ctor.position;
2697 } else if line.end == point_id {
2698 line_ctor.end = ctor.position;
2699 } else {
2700 return Err(KclError::refactor(format!(
2701 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2702 )));
2703 }
2704 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2705 }
2706
2707 if let Segment::Arc(arc) = segment {
2709 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2710 return Err(KclError::refactor(format!(
2711 "Internal: Owner of point does not have arc ctor, but found {}",
2712 arc.ctor.human_friendly_kind_with_article()
2713 )));
2714 };
2715 let mut arc_ctor = arc_ctor.clone();
2716 if arc.center == point_id {
2718 arc_ctor.center = ctor.position;
2719 } else if arc.start == point_id {
2720 arc_ctor.start = ctor.position;
2721 } else if arc.end == point_id {
2722 arc_ctor.end = ctor.position;
2723 } else {
2724 return Err(KclError::refactor(format!(
2725 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2726 )));
2727 }
2728 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2729 }
2730
2731 if let Segment::Circle(circle) = segment {
2733 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2734 return Err(KclError::refactor(format!(
2735 "Internal: Owner of point does not have circle ctor, but found {}",
2736 circle.ctor.human_friendly_kind_with_article()
2737 )));
2738 };
2739 let mut circle_ctor = circle_ctor.clone();
2740 if circle.center == point_id {
2741 circle_ctor.center = ctor.position;
2742 } else if circle.start == point_id {
2743 circle_ctor.start = ctor.position;
2744 } else {
2745 return Err(KclError::refactor(format!(
2746 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2747 )));
2748 }
2749 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2750 }
2751
2752 if let Segment::ControlPointSpline(spline) = segment {
2753 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2754 return Err(KclError::refactor(format!(
2755 "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2756 spline.ctor.human_friendly_kind_with_article()
2757 )));
2758 };
2759 let mut spline_ctor = spline_ctor.clone();
2760 let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2761 return Err(KclError::refactor(format!(
2762 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2763 )));
2764 };
2765 spline_ctor.points[control_index] = ctor.position;
2766 return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2767 }
2768
2769 }
2772
2773 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2775 Ok(())
2776 }
2777
2778 fn edit_line(
2779 &mut self,
2780 new_ast: &mut ast::Node<ast::Program>,
2781 sketch: ObjectId,
2782 line: ObjectId,
2783 ctor: LineCtor,
2784 ) -> Result<(), KclError> {
2785 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2787 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2788
2789 let sketch_id = sketch;
2791 let sketch_object = self
2792 .scene_graph
2793 .objects
2794 .get(sketch_id.0)
2795 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2796 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2797 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2798 };
2799 sketch
2800 .segments
2801 .iter()
2802 .find(|o| **o == line)
2803 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2804 let line_id = line;
2806 let line_object = self
2807 .scene_graph
2808 .objects
2809 .get(line_id.0)
2810 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2811 let ObjectKind::Segment { .. } = &line_object.kind else {
2812 let kind = line_object.kind.human_friendly_kind_with_article();
2813 return Err(KclError::refactor(format!(
2814 "This constraint only works on Segments, but you selected {kind}"
2815 )));
2816 };
2817
2818 self.mutate_ast(
2820 new_ast,
2821 line_id,
2822 AstMutateCommand::EditLine {
2823 start: new_start_ast,
2824 end: new_end_ast,
2825 construction: ctor.construction,
2826 },
2827 )?;
2828 Ok(())
2829 }
2830
2831 fn edit_arc(
2832 &mut self,
2833 new_ast: &mut ast::Node<ast::Program>,
2834 sketch: ObjectId,
2835 arc: ObjectId,
2836 ctor: ArcCtor,
2837 ) -> Result<(), KclError> {
2838 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2840 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2841 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2842
2843 let sketch_id = sketch;
2845 let sketch_object = self
2846 .scene_graph
2847 .objects
2848 .get(sketch_id.0)
2849 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2850 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2851 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2852 };
2853 sketch
2854 .segments
2855 .iter()
2856 .find(|o| **o == arc)
2857 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2858 let arc_id = arc;
2860 let arc_object = self
2861 .scene_graph
2862 .objects
2863 .get(arc_id.0)
2864 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2865 let ObjectKind::Segment { .. } = &arc_object.kind else {
2866 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2867 };
2868
2869 self.mutate_ast(
2871 new_ast,
2872 arc_id,
2873 AstMutateCommand::EditArc {
2874 start: new_start_ast,
2875 end: new_end_ast,
2876 center: new_center_ast,
2877 construction: ctor.construction,
2878 },
2879 )?;
2880 Ok(())
2881 }
2882
2883 fn edit_circle(
2884 &mut self,
2885 new_ast: &mut ast::Node<ast::Program>,
2886 sketch: ObjectId,
2887 circle: ObjectId,
2888 ctor: CircleCtor,
2889 ) -> Result<(), KclError> {
2890 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2892 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2893
2894 let sketch_id = sketch;
2896 let sketch_object = self
2897 .scene_graph
2898 .objects
2899 .get(sketch_id.0)
2900 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2901 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2902 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2903 };
2904 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2905 KclError::refactor(format!(
2906 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2907 ))
2908 })?;
2909 let circle_id = circle;
2911 let circle_object = self
2912 .scene_graph
2913 .objects
2914 .get(circle_id.0)
2915 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2916 let ObjectKind::Segment { .. } = &circle_object.kind else {
2917 return Err(KclError::refactor(format!(
2918 "Object is not a segment: {circle_object:?}"
2919 )));
2920 };
2921
2922 self.mutate_ast(
2924 new_ast,
2925 circle_id,
2926 AstMutateCommand::EditCircle {
2927 start: new_start_ast,
2928 center: new_center_ast,
2929 construction: ctor.construction,
2930 },
2931 )?;
2932 Ok(())
2933 }
2934
2935 fn edit_control_point_spline(
2936 &mut self,
2937 new_ast: &mut ast::Node<ast::Program>,
2938 sketch: ObjectId,
2939 spline: ObjectId,
2940 ctor: ControlPointSplineCtor,
2941 ) -> Result<(), KclError> {
2942 let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
2943
2944 let sketch_object = self
2945 .scene_graph
2946 .objects
2947 .get(sketch.0)
2948 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2949 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2950 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2951 };
2952 sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
2953 KclError::refactor(format!(
2954 "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
2955 ))
2956 })?;
2957
2958 let spline_object =
2959 self.scene_graph.objects.get(spline.0).ok_or_else(|| {
2960 KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
2961 })?;
2962 let ObjectKind::Segment { .. } = &spline_object.kind else {
2963 return Err(KclError::refactor(format!(
2964 "Object is not a segment: {spline_object:?}"
2965 )));
2966 };
2967
2968 self.mutate_ast(
2969 new_ast,
2970 spline,
2971 AstMutateCommand::EditControlPointSpline {
2972 points: points_ast,
2973 construction: ctor.construction,
2974 },
2975 )?;
2976 Ok(())
2977 }
2978
2979 fn delete_segment(
2980 &mut self,
2981 new_ast: &mut ast::Node<ast::Program>,
2982 sketch: ObjectId,
2983 segment_id: ObjectId,
2984 ) -> Result<(), KclError> {
2985 let sketch_id = sketch;
2987 let sketch_object = self
2988 .scene_graph
2989 .objects
2990 .get(sketch_id.0)
2991 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2992 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2993 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2994 };
2995 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2996 KclError::refactor(format!(
2997 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2998 ))
2999 })?;
3000 let segment_object =
3002 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3003 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3004 })?;
3005 let ObjectKind::Segment { .. } = &segment_object.kind else {
3006 return Err(KclError::refactor(format!(
3007 "Object is not a segment, it is {}",
3008 segment_object.kind.human_friendly_kind_with_article()
3009 )));
3010 };
3011
3012 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3014 Ok(())
3015 }
3016
3017 fn delete_constraint(
3018 &mut self,
3019 new_ast: &mut ast::Node<ast::Program>,
3020 sketch: ObjectId,
3021 constraint_id: ObjectId,
3022 ) -> Result<(), KclError> {
3023 let sketch_id = sketch;
3025 let sketch_object = self
3026 .scene_graph
3027 .objects
3028 .get(sketch_id.0)
3029 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3030 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3031 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3032 };
3033 sketch
3034 .constraints
3035 .iter()
3036 .find(|o| **o == constraint_id)
3037 .ok_or_else(|| {
3038 KclError::refactor(format!(
3039 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3040 ))
3041 })?;
3042 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3044 KclError::refactor(format!(
3045 "Constraint not found in scene graph: constraint={constraint_id:?}"
3046 ))
3047 })?;
3048 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3049 return Err(KclError::refactor(format!(
3050 "Object is not a constraint, it is {}",
3051 constraint_object.kind.human_friendly_kind_with_article()
3052 )));
3053 };
3054
3055 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3057 Ok(())
3058 }
3059
3060 fn edit_coincident_constraint(
3061 &mut self,
3062 new_ast: &mut ast::Node<ast::Program>,
3063 constraint_id: ObjectId,
3064 segments: Vec<ConstraintSegment>,
3065 ) -> Result<(), KclError> {
3066 if segments.len() < 2 {
3067 return Err(KclError::refactor(format!(
3068 "Coincident constraint must have at least 2 inputs, got {}",
3069 segments.len()
3070 )));
3071 }
3072
3073 let segment_asts = segments
3074 .iter()
3075 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3076 .collect::<Result<Vec<_>, _>>()?;
3077
3078 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3079 elements: segment_asts,
3080 digest: None,
3081 non_code_meta: Default::default(),
3082 })));
3083
3084 self.mutate_ast(
3085 new_ast,
3086 constraint_id,
3087 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3088 )?;
3089 Ok(())
3090 }
3091
3092 fn edit_horizontal_points_constraint(
3093 &mut self,
3094 new_ast: &mut ast::Node<ast::Program>,
3095 constraint_id: ObjectId,
3096 points: Vec<ConstraintSegment>,
3097 ) -> Result<(), KclError> {
3098 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3099 }
3100
3101 fn edit_vertical_points_constraint(
3102 &mut self,
3103 new_ast: &mut ast::Node<ast::Program>,
3104 constraint_id: ObjectId,
3105 points: Vec<ConstraintSegment>,
3106 ) -> Result<(), KclError> {
3107 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3108 }
3109
3110 fn edit_axis_points_constraint(
3111 &mut self,
3112 new_ast: &mut ast::Node<ast::Program>,
3113 constraint_id: ObjectId,
3114 points: Vec<ConstraintSegment>,
3115 constraint_name: &str,
3116 ) -> Result<(), KclError> {
3117 if points.len() < 2 {
3118 return Err(KclError::refactor(format!(
3119 "{constraint_name} points constraint must have at least 2 points, got {}",
3120 points.len()
3121 )));
3122 }
3123
3124 let point_asts = points
3125 .iter()
3126 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3127 .collect::<Result<Vec<_>, _>>()?;
3128
3129 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3130 elements: point_asts,
3131 digest: None,
3132 non_code_meta: Default::default(),
3133 })));
3134
3135 self.mutate_ast(
3136 new_ast,
3137 constraint_id,
3138 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3139 )?;
3140 Ok(())
3141 }
3142
3143 fn edit_equal_length_constraint(
3145 &mut self,
3146 new_ast: &mut ast::Node<ast::Program>,
3147 constraint_id: ObjectId,
3148 lines: Vec<ObjectId>,
3149 ) -> Result<(), KclError> {
3150 if lines.len() < 2 {
3151 return Err(KclError::refactor(format!(
3152 "Lines equal length constraint must have at least 2 lines, got {}",
3153 lines.len()
3154 )));
3155 }
3156
3157 let line_asts = lines
3158 .iter()
3159 .map(|line_id| {
3160 let line_object = self
3161 .scene_graph
3162 .objects
3163 .get(line_id.0)
3164 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3165 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3166 let kind = line_object.kind.human_friendly_kind_with_article();
3167 return Err(KclError::refactor(format!(
3168 "This constraint only works on Segments, but you selected {kind}"
3169 )));
3170 };
3171 let Segment::Line(_) = line_segment else {
3172 let kind = line_segment.human_friendly_kind_with_article();
3173 return Err(KclError::refactor(format!(
3174 "Only lines can be made equal length, but you selected {kind}"
3175 )));
3176 };
3177
3178 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3179 })
3180 .collect::<Result<Vec<_>, _>>()?;
3181
3182 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3183 elements: line_asts,
3184 digest: None,
3185 non_code_meta: Default::default(),
3186 })));
3187
3188 self.mutate_ast(
3189 new_ast,
3190 constraint_id,
3191 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3192 )?;
3193 Ok(())
3194 }
3195
3196 fn edit_parallel_constraint(
3198 &mut self,
3199 new_ast: &mut ast::Node<ast::Program>,
3200 constraint_id: ObjectId,
3201 lines: Vec<ObjectId>,
3202 ) -> Result<(), KclError> {
3203 if lines.len() < 2 {
3204 return Err(KclError::refactor(format!(
3205 "Parallel constraint must have at least 2 lines, got {}",
3206 lines.len()
3207 )));
3208 }
3209
3210 let line_asts = lines
3211 .iter()
3212 .map(|line_id| {
3213 let line_object = self
3214 .scene_graph
3215 .objects
3216 .get(line_id.0)
3217 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3218 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3219 let kind = line_object.kind.human_friendly_kind_with_article();
3220 return Err(KclError::refactor(format!(
3221 "This constraint only works on Segments, but you selected {kind}"
3222 )));
3223 };
3224 let Segment::Line(_) = line_segment else {
3225 let kind = line_segment.human_friendly_kind_with_article();
3226 return Err(KclError::refactor(format!(
3227 "Only lines can be made parallel, but you selected {kind}"
3228 )));
3229 };
3230
3231 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3232 })
3233 .collect::<Result<Vec<_>, _>>()?;
3234
3235 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3236 elements: line_asts,
3237 digest: None,
3238 non_code_meta: Default::default(),
3239 })));
3240
3241 self.mutate_ast(
3242 new_ast,
3243 constraint_id,
3244 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3245 )?;
3246 Ok(())
3247 }
3248
3249 fn edit_equal_radius_constraint(
3251 &mut self,
3252 new_ast: &mut ast::Node<ast::Program>,
3253 constraint_id: ObjectId,
3254 input: Vec<ObjectId>,
3255 ) -> Result<(), KclError> {
3256 if input.len() < 2 {
3257 return Err(KclError::refactor(format!(
3258 "equalRadius constraint must have at least 2 segments, got {}",
3259 input.len()
3260 )));
3261 }
3262
3263 let input_asts = input
3264 .iter()
3265 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3266 .collect::<Result<Vec<_>, _>>()?;
3267
3268 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3269 elements: input_asts,
3270 digest: None,
3271 non_code_meta: Default::default(),
3272 })));
3273
3274 self.mutate_ast(
3275 new_ast,
3276 constraint_id,
3277 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3278 )?;
3279 Ok(())
3280 }
3281
3282 async fn execute_after_edit(
3283 &mut self,
3284 ctx: &ExecutorContext,
3285 sketch: ObjectId,
3286 sketch_block_ref: AstNodeRef,
3287 new_ast: &mut ast::Node<ast::Program>,
3288 options: ExecuteAfterEditOptions,
3289 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3290 let ExecuteAfterEditOptions {
3291 segment_ids_edited,
3292 edit_kind,
3293 commit_solved_initial_guesses,
3294 } = options;
3295
3296 let new_source = source_from_ast(new_ast);
3298 let (new_program, errors) = Program::parse(&new_source)
3300 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3301 if !errors.is_empty() {
3302 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3303 "Error parsing KCL source after editing: {errors:?}"
3304 ))));
3305 }
3306 let Some(new_program) = new_program else {
3307 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3308 "No AST produced after editing".to_string(),
3309 )));
3310 };
3311
3312 self.program = new_program.clone();
3314
3315 let is_delete = edit_kind.is_delete();
3317 let truncated_program = {
3318 let mut truncated_program = new_program;
3319 only_sketch_block(
3320 &mut truncated_program.ast,
3321 &sketch_block_ref,
3322 edit_kind.to_change_kind(),
3323 )
3324 .map_err(KclErrorWithOutputs::no_outputs)?;
3325 truncated_program
3326 };
3327
3328 let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3330 let mock_config = MockConfig {
3331 sketch_block_id: Some(sketch),
3332 freedom_analysis: is_delete,
3333 segment_ids_edited: segment_ids_edited.clone(),
3334 drag_anchors,
3335 ..Default::default()
3336 };
3337 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3338
3339 let outcome = self.update_state_after_exec(outcome, is_delete);
3341
3342 let src_delta = if commit_solved_initial_guesses {
3343 self.commit_var_solutions_to_program(&outcome, "editing")?
3344 } else {
3345 SourceDelta { text: new_source }
3346 };
3347 let scene_graph_delta = SceneGraphDelta {
3348 new_graph: self.scene_graph_for_ui(),
3349 invalidates_ids: is_delete,
3350 new_objects: Vec::new(),
3351 exec_outcome: outcome,
3352 };
3353 Ok((src_delta, scene_graph_delta))
3354 }
3355
3356 async fn execute_after_delete_sketch(
3357 &mut self,
3358 ctx: &ExecutorContext,
3359 new_ast: &mut ast::Node<ast::Program>,
3360 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3361 let new_source = source_from_ast(new_ast);
3363 let (new_program, errors) = Program::parse(&new_source)
3365 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3366 if !errors.is_empty() {
3367 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3368 "Error parsing KCL source after editing: {errors:?}"
3369 ))));
3370 }
3371 let Some(new_program) = new_program else {
3372 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3373 "No AST produced after editing".to_string(),
3374 )));
3375 };
3376
3377 self.program = new_program.clone();
3379
3380 let outcome = ctx.run_with_caching(new_program).await?;
3386 let freedom_analysis_ran = true;
3387
3388 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3389
3390 let src_delta = SourceDelta { text: new_source };
3391 let scene_graph_delta = SceneGraphDelta {
3392 new_graph: self.scene_graph_for_ui(),
3393 invalidates_ids: true,
3394 new_objects: Vec::new(),
3395 exec_outcome: outcome,
3396 };
3397 Ok((src_delta, scene_graph_delta))
3398 }
3399
3400 fn point_id_to_ast_reference(
3405 &self,
3406 point_id: ObjectId,
3407 new_ast: &mut ast::Node<ast::Program>,
3408 ) -> Result<ast::Expr, KclError> {
3409 let point_object = self
3410 .scene_graph
3411 .objects
3412 .get(point_id.0)
3413 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3414 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3415 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3416 };
3417 let Segment::Point(point) = point_segment else {
3418 return Err(KclError::refactor(format!(
3419 "Only points are currently supported: {point_object:?}"
3420 )));
3421 };
3422
3423 if let Some(owner_id) = point.owner {
3424 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3425 KclError::refactor(format!(
3426 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3427 ))
3428 })?;
3429 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3430 return Err(KclError::refactor(format!(
3431 "Owner of point is not a segment, but found {}",
3432 owner_object.kind.human_friendly_kind_with_article()
3433 )));
3434 };
3435
3436 match owner_segment {
3437 Segment::Line(line) => {
3438 let property = if line.start == point_id {
3439 LINE_PROPERTY_START
3440 } else if line.end == point_id {
3441 LINE_PROPERTY_END
3442 } else {
3443 return Err(KclError::refactor(format!(
3444 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3445 )));
3446 };
3447 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3448 }
3449 Segment::Arc(arc) => {
3450 let property = if arc.start == point_id {
3451 ARC_PROPERTY_START
3452 } else if arc.end == point_id {
3453 ARC_PROPERTY_END
3454 } else if arc.center == point_id {
3455 ARC_PROPERTY_CENTER
3456 } else {
3457 return Err(KclError::refactor(format!(
3458 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3459 )));
3460 };
3461 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3462 }
3463 Segment::Circle(circle) => {
3464 let property = if circle.start == point_id {
3465 CIRCLE_PROPERTY_START
3466 } else if circle.center == point_id {
3467 CIRCLE_PROPERTY_CENTER
3468 } else {
3469 return Err(KclError::refactor(format!(
3470 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3471 )));
3472 };
3473 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3474 }
3475 Segment::ControlPointSpline(spline) => {
3476 let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3477 return Err(KclError::refactor(format!(
3478 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3479 )));
3480 };
3481 let owner_expr =
3482 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3483 let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3484 Ok(create_index_expression(controls_expr, index))
3485 }
3486 _ => Err(KclError::refactor(format!(
3487 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3488 ))),
3489 }
3490 } else {
3491 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3493 }
3494 }
3495
3496 fn line_id_to_ast_reference(
3497 &self,
3498 line_id: ObjectId,
3499 new_ast: &mut ast::Node<ast::Program>,
3500 ) -> Result<ast::Expr, KclError> {
3501 let line_object = self
3502 .scene_graph
3503 .objects
3504 .get(line_id.0)
3505 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3506 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3507 return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3508 };
3509 let Segment::Line(line) = line_segment else {
3510 return Err(KclError::refactor(format!(
3511 "Only lines are currently supported: {line_object:?}"
3512 )));
3513 };
3514
3515 if let Some(owner_id) = line.owner {
3516 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3517 KclError::refactor(format!(
3518 "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3519 ))
3520 })?;
3521 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3522 return Err(KclError::refactor(format!(
3523 "Owner of line is not a segment, but found {}",
3524 owner_object.kind.human_friendly_kind_with_article()
3525 )));
3526 };
3527
3528 match owner_segment {
3529 Segment::ControlPointSpline(spline) => {
3530 let Some(index) = spline
3531 .controls
3532 .windows(2)
3533 .position(|window| window[0] == line.start && window[1] == line.end)
3534 else {
3535 return Err(KclError::refactor(format!(
3536 "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3537 )));
3538 };
3539 let owner_expr =
3540 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3541 let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3542 Ok(create_index_expression(edges_expr, index))
3543 }
3544 _ => Err(KclError::refactor(format!(
3545 "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3546 ))),
3547 }
3548 } else {
3549 get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3550 }
3551 }
3552
3553 fn coincident_segment_to_ast(
3554 &self,
3555 segment: &ConstraintSegment,
3556 new_ast: &mut ast::Node<ast::Program>,
3557 ) -> Result<ast::Expr, KclError> {
3558 match segment {
3559 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3560 ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3561 }
3562 }
3563
3564 fn segment_id_to_constraint_ast_reference(
3565 &self,
3566 segment_id: ObjectId,
3567 new_ast: &mut ast::Node<ast::Program>,
3568 ) -> Result<ast::Expr, KclError> {
3569 let segment_object = self
3570 .scene_graph
3571 .objects
3572 .get(segment_id.0)
3573 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3574 let ObjectKind::Segment { segment } = &segment_object.kind else {
3575 return Err(KclError::refactor(format!(
3576 "Object is not a segment, it is {}",
3577 segment_object.kind.human_friendly_kind_with_article()
3578 )));
3579 };
3580
3581 match segment {
3582 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3583 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3584 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3585 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3586 Segment::ControlPointSpline(_) => {
3587 get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3588 }
3589 }
3590 }
3591
3592 fn axis_constraint_segment_to_ast(
3593 &self,
3594 segment: &ConstraintSegment,
3595 new_ast: &mut ast::Node<ast::Program>,
3596 ) -> Result<ast::Expr, KclError> {
3597 match segment {
3598 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3599 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3600 }
3601 }
3602
3603 async fn add_coincident(
3604 &mut self,
3605 sketch: ObjectId,
3606 coincident: Coincident,
3607 new_ast: &mut ast::Node<ast::Program>,
3608 ) -> Result<AstNodeRef, KclError> {
3609 let sketch_id = sketch;
3610 for segment in &coincident.segments {
3611 let ConstraintSegment::Segment(segment_id) = segment else {
3612 continue;
3613 };
3614 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3615 continue;
3616 };
3617 if matches!(
3618 segment_object.kind,
3619 ObjectKind::Segment {
3620 segment: Segment::ControlPointSpline(_)
3621 }
3622 ) {
3623 return Err(KclError::refactor(
3624 "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3625 .to_owned(),
3626 ));
3627 }
3628 }
3629 let segment_asts = coincident
3630 .segments
3631 .iter()
3632 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3633 .collect::<Result<Vec<_>, _>>()?;
3634 if segment_asts.len() < 2 {
3635 return Err(KclError::refactor(format!(
3636 "Coincident constraint must have at least 2 inputs, got {}",
3637 segment_asts.len()
3638 )));
3639 }
3640
3641 let coincident_ast = create_coincident_ast(segment_asts);
3643
3644 let (sketch_block_ref, _) = self.mutate_ast(
3646 new_ast,
3647 sketch_id,
3648 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3649 )?;
3650 Ok(sketch_block_ref)
3651 }
3652
3653 async fn add_distance(
3654 &mut self,
3655 sketch: ObjectId,
3656 distance: Distance,
3657 new_ast: &mut ast::Node<ast::Program>,
3658 ) -> Result<AstNodeRef, KclError> {
3659 let sketch_id = sketch;
3660 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3661 [pt0, pt1] => [
3662 self.coincident_segment_to_ast(pt0, new_ast)?,
3663 self.coincident_segment_to_ast(pt1, new_ast)?,
3664 ],
3665 _ => {
3666 return Err(KclError::refactor(format!(
3667 "Distance constraint must have exactly 2 points, got {}",
3668 distance.points.len()
3669 )));
3670 }
3671 };
3672
3673 let arguments = match &distance.label_position {
3674 Some(label_position) => vec![ast::LabeledArg {
3675 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3676 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3677 }],
3678 None => Default::default(),
3679 };
3680
3681 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3683 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3684 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3685 ast::ArrayExpression {
3686 elements: vec![pt0_ast, pt1_ast],
3687 digest: None,
3688 non_code_meta: Default::default(),
3689 },
3690 )))),
3691 arguments,
3692 digest: None,
3693 non_code_meta: Default::default(),
3694 })));
3695 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3696 left: distance_call_ast,
3697 operator: ast::BinaryOperator::Eq,
3698 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3699 value: ast::LiteralValue::Number {
3700 value: distance.distance.value,
3701 suffix: distance.distance.units,
3702 },
3703 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3704 KclError::refactor(format!(
3705 "Could not format numeric suffix: {:?}",
3706 distance.distance.units
3707 ))
3708 })?,
3709 digest: None,
3710 }))),
3711 digest: None,
3712 })));
3713
3714 let (sketch_block_ref, _) = self.mutate_ast(
3716 new_ast,
3717 sketch_id,
3718 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3719 )?;
3720 Ok(sketch_block_ref)
3721 }
3722
3723 async fn add_angle(
3724 &mut self,
3725 sketch: ObjectId,
3726 angle: Angle,
3727 new_ast: &mut ast::Node<ast::Program>,
3728 ) -> Result<AstNodeRef, KclError> {
3729 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3730 return Err(KclError::refactor(format!(
3731 "Angle constraint must have exactly 2 lines, got {}",
3732 angle.lines.len()
3733 )));
3734 };
3735 let sketch_id = sketch;
3736
3737 let line0_object = self
3739 .scene_graph
3740 .objects
3741 .get(l0_id.0)
3742 .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3743 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3744 return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3745 };
3746 let Segment::Line(_) = line0_segment else {
3747 return Err(KclError::refactor(format!(
3748 "Only lines can be constrained to meet at an angle: {line0_object:?}",
3749 )));
3750 };
3751 let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3752
3753 let line1_object = self
3754 .scene_graph
3755 .objects
3756 .get(l1_id.0)
3757 .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3758 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3759 return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3760 };
3761 let Segment::Line(_) = line1_segment else {
3762 return Err(KclError::refactor(format!(
3763 "Only lines can be constrained to meet at an angle: {line1_object:?}",
3764 )));
3765 };
3766 let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3767
3768 let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3770 callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3771 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3772 ast::ArrayExpression {
3773 elements: vec![l0_ast, l1_ast],
3774 digest: None,
3775 non_code_meta: Default::default(),
3776 },
3777 )))),
3778 arguments: Default::default(),
3779 digest: None,
3780 non_code_meta: Default::default(),
3781 })));
3782 let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3783 left: angle_call_ast,
3784 operator: ast::BinaryOperator::Eq,
3785 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3786 value: ast::LiteralValue::Number {
3787 value: angle.angle.value,
3788 suffix: angle.angle.units,
3789 },
3790 raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3791 KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3792 })?,
3793 digest: None,
3794 }))),
3795 digest: None,
3796 })));
3797
3798 let (sketch_block_ref, _) = self.mutate_ast(
3800 new_ast,
3801 sketch_id,
3802 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3803 )?;
3804 Ok(sketch_block_ref)
3805 }
3806
3807 async fn add_tangent(
3808 &mut self,
3809 sketch: ObjectId,
3810 tangent: Tangent,
3811 new_ast: &mut ast::Node<ast::Program>,
3812 ) -> Result<AstNodeRef, KclError> {
3813 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3814 return Err(KclError::refactor(format!(
3815 "Tangent constraint must have exactly 2 segments, got {}",
3816 tangent.input.len()
3817 )));
3818 };
3819 let sketch_id = sketch;
3820
3821 let seg0_object = self
3822 .scene_graph
3823 .objects
3824 .get(seg0_id.0)
3825 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3826 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3827 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3828 };
3829 let seg0_ast = match seg0_segment {
3830 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3831 self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3832 }
3833 _ => {
3834 return Err(KclError::refactor(format!(
3835 "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3836 )));
3837 }
3838 };
3839
3840 let seg1_object = self
3841 .scene_graph
3842 .objects
3843 .get(seg1_id.0)
3844 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3845 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3846 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3847 };
3848 let seg1_ast = match seg1_segment {
3849 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3850 self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3851 }
3852 _ => {
3853 return Err(KclError::refactor(format!(
3854 "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3855 )));
3856 }
3857 };
3858
3859 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3860 let (sketch_block_ref, _) = self.mutate_ast(
3861 new_ast,
3862 sketch_id,
3863 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3864 )?;
3865 Ok(sketch_block_ref)
3866 }
3867
3868 async fn add_symmetric(
3869 &mut self,
3870 sketch: ObjectId,
3871 symmetric: Symmetric,
3872 new_ast: &mut ast::Node<ast::Program>,
3873 ) -> Result<AstNodeRef, KclError> {
3874 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3875 return Err(KclError::refactor(format!(
3876 "Symmetric constraint must have exactly 2 inputs, got {}",
3877 symmetric.input.len()
3878 )));
3879 };
3880 let sketch_id = sketch;
3881
3882 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3883 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3884 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3885
3886 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3887 let (sketch_block_ref, _) = self.mutate_ast(
3888 new_ast,
3889 sketch_id,
3890 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3891 )?;
3892 Ok(sketch_block_ref)
3893 }
3894
3895 async fn add_midpoint(
3896 &mut self,
3897 sketch: ObjectId,
3898 midpoint: Midpoint,
3899 new_ast: &mut ast::Node<ast::Program>,
3900 ) -> Result<AstNodeRef, KclError> {
3901 let sketch_id = sketch;
3902 let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
3903
3904 let segment_object = self
3905 .scene_graph
3906 .objects
3907 .get(midpoint.segment.0)
3908 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3909 let ObjectKind::Segment {
3910 segment: midpoint_segment,
3911 } = &segment_object.kind
3912 else {
3913 return Err(KclError::refactor(format!(
3914 "Object must be a segment, but it was {}",
3915 segment_object.kind.human_friendly_kind_with_article()
3916 )));
3917 };
3918 let segment_ast = match midpoint_segment {
3919 Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
3920 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3921 _ => {
3922 return Err(KclError::refactor(format!(
3923 "Midpoint target must be a line or arc segment but it was {}",
3924 midpoint_segment.human_friendly_kind_with_article()
3925 )));
3926 }
3927 };
3928
3929 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3930 let (sketch_block_ref, _) = self.mutate_ast(
3931 new_ast,
3932 sketch_id,
3933 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3934 )?;
3935 Ok(sketch_block_ref)
3936 }
3937
3938 async fn add_equal_radius(
3939 &mut self,
3940 sketch: ObjectId,
3941 equal_radius: EqualRadius,
3942 new_ast: &mut ast::Node<ast::Program>,
3943 ) -> Result<AstNodeRef, KclError> {
3944 if equal_radius.input.len() < 2 {
3945 return Err(KclError::refactor(format!(
3946 "equalRadius constraint must have at least 2 segments, got {}",
3947 equal_radius.input.len()
3948 )));
3949 }
3950
3951 let sketch_id = sketch;
3952 let input_asts = equal_radius
3953 .input
3954 .iter()
3955 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3956 .collect::<Result<Vec<_>, _>>()?;
3957
3958 let equal_radius_ast = create_equal_radius_ast(input_asts);
3959 let (sketch_block_ref, _) = self.mutate_ast(
3960 new_ast,
3961 sketch_id,
3962 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3963 )?;
3964 Ok(sketch_block_ref)
3965 }
3966
3967 async fn add_radius(
3968 &mut self,
3969 sketch: ObjectId,
3970 radius: Radius,
3971 new_ast: &mut ast::Node<ast::Program>,
3972 ) -> Result<AstNodeRef, KclError> {
3973 let params = ArcSizeConstraintParams {
3974 points: vec![radius.arc],
3975 function_name: RADIUS_FN,
3976 value: radius.radius.value,
3977 units: radius.radius.units,
3978 label_position: radius.label_position,
3979 constraint_type_name: "Radius",
3980 };
3981 self.add_arc_size_constraint(sketch, params, new_ast).await
3982 }
3983
3984 async fn add_diameter(
3985 &mut self,
3986 sketch: ObjectId,
3987 diameter: Diameter,
3988 new_ast: &mut ast::Node<ast::Program>,
3989 ) -> Result<AstNodeRef, KclError> {
3990 let params = ArcSizeConstraintParams {
3991 points: vec![diameter.arc],
3992 function_name: DIAMETER_FN,
3993 value: diameter.diameter.value,
3994 units: diameter.diameter.units,
3995 label_position: diameter.label_position,
3996 constraint_type_name: "Diameter",
3997 };
3998 self.add_arc_size_constraint(sketch, params, new_ast).await
3999 }
4000
4001 async fn add_fixed_constraints(
4002 &mut self,
4003 sketch: ObjectId,
4004 points: Vec<FixedPoint>,
4005 new_ast: &mut ast::Node<ast::Program>,
4006 ) -> Result<AstNodeRef, KclError> {
4007 let mut sketch_block_ref = None;
4008
4009 for fixed_point in points {
4010 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4011 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4012 .map_err(|err| KclError::refactor(err.to_string()))?;
4013
4014 let (sketch_ref, _) = self.mutate_ast(
4015 new_ast,
4016 sketch,
4017 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4018 )?;
4019 sketch_block_ref = Some(sketch_ref);
4020 }
4021
4022 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4023 }
4024
4025 async fn add_arc_size_constraint(
4026 &mut self,
4027 sketch: ObjectId,
4028 params: ArcSizeConstraintParams,
4029 new_ast: &mut ast::Node<ast::Program>,
4030 ) -> Result<AstNodeRef, KclError> {
4031 let sketch_id = sketch;
4032
4033 if params.points.len() != 1 {
4035 return Err(KclError::refactor(format!(
4036 "{} constraint must have exactly 1 argument (an arc segment), got {}",
4037 params.constraint_type_name,
4038 params.points.len()
4039 )));
4040 }
4041
4042 let arc_id = params.points[0];
4043 let arc_object = self
4044 .scene_graph
4045 .objects
4046 .get(arc_id.0)
4047 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4048 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4049 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4050 };
4051 let ref_type = match arc_segment {
4052 Segment::Arc(_) => ARC_VARIABLE,
4053 Segment::Circle(_) => CIRCLE_VARIABLE,
4054 _ => {
4055 return Err(KclError::refactor(format!(
4056 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4057 params.constraint_type_name
4058 )));
4059 }
4060 };
4061 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4063 let arguments = match ¶ms.label_position {
4064 Some(label_position) => vec![ast::LabeledArg {
4065 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4066 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4067 }],
4068 None => Default::default(),
4069 };
4070
4071 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4073 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4074 unlabeled: Some(arc_ast),
4075 arguments,
4076 digest: None,
4077 non_code_meta: Default::default(),
4078 })));
4079 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4080 left: call_ast,
4081 operator: ast::BinaryOperator::Eq,
4082 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4083 value: ast::LiteralValue::Number {
4084 value: params.value,
4085 suffix: params.units,
4086 },
4087 raw: format_number_literal(params.value, params.units, None)
4088 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4089 digest: None,
4090 }))),
4091 digest: None,
4092 })));
4093
4094 let (sketch_block_ref, _) = self.mutate_ast(
4096 new_ast,
4097 sketch_id,
4098 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4099 )?;
4100 Ok(sketch_block_ref)
4101 }
4102
4103 async fn add_horizontal_distance(
4104 &mut self,
4105 sketch: ObjectId,
4106 distance: Distance,
4107 new_ast: &mut ast::Node<ast::Program>,
4108 ) -> Result<AstNodeRef, KclError> {
4109 let sketch_id = sketch;
4110 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4111 [pt0, pt1] => [
4112 self.coincident_segment_to_ast(pt0, new_ast)?,
4113 self.coincident_segment_to_ast(pt1, new_ast)?,
4114 ],
4115 _ => {
4116 return Err(KclError::refactor(format!(
4117 "Horizontal distance constraint must have exactly 2 points, got {}",
4118 distance.points.len()
4119 )));
4120 }
4121 };
4122
4123 let arguments = match &distance.label_position {
4124 Some(label_position) => vec![ast::LabeledArg {
4125 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4126 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4127 }],
4128 None => Default::default(),
4129 };
4130
4131 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4133 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
4134 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4135 ast::ArrayExpression {
4136 elements: vec![pt0_ast, pt1_ast],
4137 digest: None,
4138 non_code_meta: Default::default(),
4139 },
4140 )))),
4141 arguments,
4142 digest: None,
4143 non_code_meta: Default::default(),
4144 })));
4145 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4146 left: distance_call_ast,
4147 operator: ast::BinaryOperator::Eq,
4148 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4149 value: ast::LiteralValue::Number {
4150 value: distance.distance.value,
4151 suffix: distance.distance.units,
4152 },
4153 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4154 KclError::refactor(format!(
4155 "Could not format numeric suffix: {:?}",
4156 distance.distance.units
4157 ))
4158 })?,
4159 digest: None,
4160 }))),
4161 digest: None,
4162 })));
4163
4164 let (sketch_block_ref, _) = self.mutate_ast(
4166 new_ast,
4167 sketch_id,
4168 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4169 )?;
4170 Ok(sketch_block_ref)
4171 }
4172
4173 async fn add_vertical_distance(
4174 &mut self,
4175 sketch: ObjectId,
4176 distance: Distance,
4177 new_ast: &mut ast::Node<ast::Program>,
4178 ) -> Result<AstNodeRef, KclError> {
4179 let sketch_id = sketch;
4180 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4181 [pt0, pt1] => [
4182 self.coincident_segment_to_ast(pt0, new_ast)?,
4183 self.coincident_segment_to_ast(pt1, new_ast)?,
4184 ],
4185 _ => {
4186 return Err(KclError::refactor(format!(
4187 "Vertical distance constraint must have exactly 2 points, got {}",
4188 distance.points.len()
4189 )));
4190 }
4191 };
4192
4193 let arguments = match &distance.label_position {
4194 Some(label_position) => vec![ast::LabeledArg {
4195 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4196 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4197 }],
4198 None => Default::default(),
4199 };
4200
4201 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4203 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
4204 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4205 ast::ArrayExpression {
4206 elements: vec![pt0_ast, pt1_ast],
4207 digest: None,
4208 non_code_meta: Default::default(),
4209 },
4210 )))),
4211 arguments,
4212 digest: None,
4213 non_code_meta: Default::default(),
4214 })));
4215 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4216 left: distance_call_ast,
4217 operator: ast::BinaryOperator::Eq,
4218 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4219 value: ast::LiteralValue::Number {
4220 value: distance.distance.value,
4221 suffix: distance.distance.units,
4222 },
4223 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4224 KclError::refactor(format!(
4225 "Could not format numeric suffix: {:?}",
4226 distance.distance.units
4227 ))
4228 })?,
4229 digest: None,
4230 }))),
4231 digest: None,
4232 })));
4233
4234 let (sketch_block_ref, _) = self.mutate_ast(
4236 new_ast,
4237 sketch_id,
4238 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4239 )?;
4240 Ok(sketch_block_ref)
4241 }
4242
4243 async fn add_horizontal(
4244 &mut self,
4245 sketch: ObjectId,
4246 horizontal: Horizontal,
4247 new_ast: &mut ast::Node<ast::Program>,
4248 ) -> Result<AstNodeRef, KclError> {
4249 let sketch_id = sketch;
4250
4251 let first_arg_ast = match horizontal {
4253 Horizontal::Line { line } => {
4254 let line_object = self
4255 .scene_graph
4256 .objects
4257 .get(line.0)
4258 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4259 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4260 let kind = line_object.kind.human_friendly_kind_with_article();
4261 return Err(KclError::refactor(format!(
4262 "This constraint only works on Segments, but you selected {kind}"
4263 )));
4264 };
4265 let Segment::Line(_) = line_segment else {
4266 return Err(KclError::refactor(format!(
4267 "Only lines can be made horizontal, but you selected {}",
4268 line_segment.human_friendly_kind_with_article(),
4269 )));
4270 };
4271 self.line_id_to_ast_reference(line, new_ast)?
4272 }
4273 Horizontal::Points { points } => {
4274 let point_asts = points
4275 .iter()
4276 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4277 .collect::<Result<Vec<_>, _>>()?;
4278 ast::ArrayExpression::new(point_asts).into()
4279 }
4280 };
4281 let horizontal_ast = create_horizontal_ast(first_arg_ast);
4283
4284 let (sketch_block_ref, _) = self.mutate_ast(
4286 new_ast,
4287 sketch_id,
4288 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4289 )?;
4290 Ok(sketch_block_ref)
4291 }
4292
4293 async fn add_lines_equal_length(
4294 &mut self,
4295 sketch: ObjectId,
4296 lines_equal_length: LinesEqualLength,
4297 new_ast: &mut ast::Node<ast::Program>,
4298 ) -> Result<AstNodeRef, KclError> {
4299 if lines_equal_length.lines.len() < 2 {
4300 return Err(KclError::refactor(format!(
4301 "Lines equal length constraint must have at least 2 lines, got {}",
4302 lines_equal_length.lines.len()
4303 )));
4304 };
4305
4306 let sketch_id = sketch;
4307
4308 let line_asts = lines_equal_length
4310 .lines
4311 .iter()
4312 .map(|line_id| {
4313 let line_object = self
4314 .scene_graph
4315 .objects
4316 .get(line_id.0)
4317 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4318 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4319 let kind = line_object.kind.human_friendly_kind_with_article();
4320 return Err(KclError::refactor(format!(
4321 "This constraint only works on Segments, but you selected {kind}"
4322 )));
4323 };
4324 let Segment::Line(_) = line_segment else {
4325 let kind = line_segment.human_friendly_kind_with_article();
4326 return Err(KclError::refactor(format!(
4327 "Only lines can be made equal length, but you selected {kind}"
4328 )));
4329 };
4330
4331 self.line_id_to_ast_reference(*line_id, new_ast)
4332 })
4333 .collect::<Result<Vec<_>, _>>()?;
4334
4335 let equal_length_ast = create_equal_length_ast(line_asts);
4337
4338 let (sketch_block_ref, _) = self.mutate_ast(
4340 new_ast,
4341 sketch_id,
4342 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4343 )?;
4344 Ok(sketch_block_ref)
4345 }
4346
4347 fn equal_radius_segment_id_to_ast_reference(
4348 &mut self,
4349 segment_id: ObjectId,
4350 new_ast: &mut ast::Node<ast::Program>,
4351 ) -> Result<ast::Expr, KclError> {
4352 let segment_object = self
4353 .scene_graph
4354 .objects
4355 .get(segment_id.0)
4356 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4357 let ObjectKind::Segment { segment } = &segment_object.kind else {
4358 return Err(KclError::refactor(format!(
4359 "Object is not a segment, it was {}",
4360 segment_object.kind.human_friendly_kind_with_article()
4361 )));
4362 };
4363
4364 let ref_type = match segment {
4365 Segment::Arc(_) => ARC_VARIABLE,
4366 Segment::Circle(_) => CIRCLE_VARIABLE,
4367 _ => {
4368 return Err(KclError::refactor(format!(
4369 "equalRadius supports only arc/circle segments, got {}",
4370 segment.human_friendly_kind_with_article()
4371 )));
4372 }
4373 };
4374
4375 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4376 }
4377
4378 fn symmetric_input_id_to_ast_reference(
4379 &mut self,
4380 segment_id: ObjectId,
4381 new_ast: &mut ast::Node<ast::Program>,
4382 ) -> Result<ast::Expr, KclError> {
4383 let segment_object = self
4384 .scene_graph
4385 .objects
4386 .get(segment_id.0)
4387 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4388 let ObjectKind::Segment { segment } = &segment_object.kind else {
4389 return Err(KclError::refactor(format!(
4390 "Object is not a segment, it was {}",
4391 segment_object.kind.human_friendly_kind_with_article()
4392 )));
4393 };
4394
4395 match segment {
4396 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4397 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4398 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4399 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4400 Segment::ControlPointSpline(_) => Err(KclError::refactor(
4401 "Symmetric does not yet support control point splines".to_owned(),
4402 )),
4403 }
4404 }
4405
4406 fn symmetric_axis_id_to_ast_reference(
4407 &mut self,
4408 segment_id: ObjectId,
4409 new_ast: &mut ast::Node<ast::Program>,
4410 ) -> Result<ast::Expr, KclError> {
4411 let segment_object = self
4412 .scene_graph
4413 .objects
4414 .get(segment_id.0)
4415 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4416 let ObjectKind::Segment { segment } = &segment_object.kind else {
4417 return Err(KclError::refactor(format!(
4418 "Object is not a segment, it was {}",
4419 segment_object.kind.human_friendly_kind_with_article()
4420 )));
4421 };
4422 match segment {
4423 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4424 _ => Err(KclError::refactor(format!(
4425 "Symmetric axis must be a line, got {}",
4426 segment.human_friendly_kind_with_article()
4427 ))),
4428 }
4429 }
4430
4431 async fn add_parallel(
4432 &mut self,
4433 sketch: ObjectId,
4434 parallel: Parallel,
4435 new_ast: &mut ast::Node<ast::Program>,
4436 ) -> Result<AstNodeRef, KclError> {
4437 if parallel.lines.len() < 2 {
4438 return Err(KclError::refactor(format!(
4439 "Parallel constraint must have at least 2 lines, got {}",
4440 parallel.lines.len()
4441 )));
4442 };
4443
4444 let sketch_id = sketch;
4445
4446 let line_asts = parallel
4447 .lines
4448 .iter()
4449 .map(|line_id| {
4450 let line_object = self
4451 .scene_graph
4452 .objects
4453 .get(line_id.0)
4454 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4455 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4456 let kind = line_object.kind.human_friendly_kind_with_article();
4457 return Err(KclError::refactor(format!(
4458 "This constraint only works on Segments, but you selected {kind}"
4459 )));
4460 };
4461 let Segment::Line(_) = line_segment else {
4462 let kind = line_segment.human_friendly_kind_with_article();
4463 return Err(KclError::refactor(format!(
4464 "Only lines can be made parallel, but you selected {kind}"
4465 )));
4466 };
4467
4468 self.line_id_to_ast_reference(*line_id, new_ast)
4469 })
4470 .collect::<Result<Vec<_>, _>>()?;
4471
4472 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4473 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4474 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4475 ast::ArrayExpression {
4476 elements: line_asts,
4477 digest: None,
4478 non_code_meta: Default::default(),
4479 },
4480 )))),
4481 arguments: Default::default(),
4482 digest: None,
4483 non_code_meta: Default::default(),
4484 })));
4485
4486 let (sketch_block_ref, _) = self.mutate_ast(
4487 new_ast,
4488 sketch_id,
4489 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4490 )?;
4491 Ok(sketch_block_ref)
4492 }
4493
4494 async fn add_perpendicular(
4495 &mut self,
4496 sketch: ObjectId,
4497 perpendicular: Perpendicular,
4498 new_ast: &mut ast::Node<ast::Program>,
4499 ) -> Result<AstNodeRef, KclError> {
4500 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4501 .await
4502 }
4503
4504 async fn add_lines_at_angle_constraint(
4505 &mut self,
4506 sketch: ObjectId,
4507 angle_kind: LinesAtAngleKind,
4508 lines: Vec<ObjectId>,
4509 new_ast: &mut ast::Node<ast::Program>,
4510 ) -> Result<AstNodeRef, KclError> {
4511 let &[line0_id, line1_id] = lines.as_slice() else {
4512 return Err(KclError::refactor(format!(
4513 "{} constraint must have exactly 2 lines, got {}",
4514 angle_kind.to_function_name(),
4515 lines.len()
4516 )));
4517 };
4518
4519 let sketch_id = sketch;
4520
4521 let line0_object = self
4523 .scene_graph
4524 .objects
4525 .get(line0_id.0)
4526 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4527 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4528 let kind = line0_object.kind.human_friendly_kind_with_article();
4529 return Err(KclError::refactor(format!(
4530 "This constraint only works on Segments, but you selected {kind}"
4531 )));
4532 };
4533 let Segment::Line(_) = line0_segment else {
4534 return Err(KclError::refactor(format!(
4535 "Only lines can be made {}, but you selected {}",
4536 angle_kind.to_function_name(),
4537 line0_segment.human_friendly_kind_with_article(),
4538 )));
4539 };
4540 let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4541
4542 let line1_object = self
4543 .scene_graph
4544 .objects
4545 .get(line1_id.0)
4546 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4547 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4548 let kind = line1_object.kind.human_friendly_kind_with_article();
4549 return Err(KclError::refactor(format!(
4550 "This constraint only works on Segments, but you selected {kind}"
4551 )));
4552 };
4553 let Segment::Line(_) = line1_segment else {
4554 return Err(KclError::refactor(format!(
4555 "Only lines can be made {}, but you selected {}",
4556 angle_kind.to_function_name(),
4557 line1_segment.human_friendly_kind_with_article(),
4558 )));
4559 };
4560 let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4561
4562 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4564 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4565 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4566 ast::ArrayExpression {
4567 elements: vec![line0_ast, line1_ast],
4568 digest: None,
4569 non_code_meta: Default::default(),
4570 },
4571 )))),
4572 arguments: Default::default(),
4573 digest: None,
4574 non_code_meta: Default::default(),
4575 })));
4576
4577 let (sketch_block_ref, _) = self.mutate_ast(
4579 new_ast,
4580 sketch_id,
4581 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4582 )?;
4583 Ok(sketch_block_ref)
4584 }
4585
4586 async fn add_vertical(
4587 &mut self,
4588 sketch: ObjectId,
4589 vertical: Vertical,
4590 new_ast: &mut ast::Node<ast::Program>,
4591 ) -> Result<AstNodeRef, KclError> {
4592 let sketch_id = sketch;
4593
4594 let first_arg_ast = match vertical {
4595 Vertical::Line { line } => {
4596 let line_object = self
4598 .scene_graph
4599 .objects
4600 .get(line.0)
4601 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4602 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4603 let kind = line_object.kind.human_friendly_kind_with_article();
4604 return Err(KclError::refactor(format!(
4605 "This constraint only works on Segments, but you selected {kind}"
4606 )));
4607 };
4608 let Segment::Line(_) = line_segment else {
4609 return Err(KclError::refactor(format!(
4610 "Only lines can be made vertical, but you selected {}",
4611 line_segment.human_friendly_kind_with_article()
4612 )));
4613 };
4614 self.line_id_to_ast_reference(line, new_ast)?
4615 }
4616 Vertical::Points { points } => {
4617 let point_asts = points
4618 .iter()
4619 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4620 .collect::<Result<Vec<_>, _>>()?;
4621 ast::ArrayExpression::new(point_asts).into()
4622 }
4623 };
4624 let vertical_ast = create_vertical_ast(first_arg_ast);
4626
4627 let (sketch_block_ref, _) = self.mutate_ast(
4629 new_ast,
4630 sketch_id,
4631 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4632 )?;
4633 Ok(sketch_block_ref)
4634 }
4635
4636 async fn execute_after_add_constraint(
4637 &mut self,
4638 ctx: &ExecutorContext,
4639 sketch_id: ObjectId,
4640 sketch_block_ref: AstNodeRef,
4641 new_ast: &mut ast::Node<ast::Program>,
4642 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4643 let new_source = source_from_ast(new_ast);
4645 let (new_program, errors) = Program::parse(&new_source)
4647 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4648 if !errors.is_empty() {
4649 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4650 "Error parsing KCL source after adding constraint: {errors:?}"
4651 ))));
4652 }
4653 let Some(new_program) = new_program else {
4654 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4655 "No AST produced after adding constraint".to_string(),
4656 )));
4657 };
4658 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4659 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4660 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4661 )))
4662 })?;
4663
4664 let mut truncated_program = new_program.clone();
4667 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4668 .map_err(KclErrorWithOutputs::no_outputs)?;
4669
4670 let outcome = ctx
4672 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4673 .await?;
4674
4675 let new_object_ids = {
4676 let constraint_id = outcome
4678 .source_range_to_object
4679 .get(&constraint_node_ref.range)
4680 .copied()
4681 .ok_or_else(|| {
4682 KclErrorWithOutputs::from_error_outcome(
4683 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4684 outcome.clone(),
4685 )
4686 })?;
4687 vec![constraint_id]
4688 };
4689
4690 self.program = new_program;
4693
4694 let outcome = self.update_state_after_exec(outcome, true);
4696
4697 let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4698 let scene_graph_delta = SceneGraphDelta {
4699 new_graph: self.scene_graph_for_ui(),
4700 invalidates_ids: false,
4701 new_objects: new_object_ids,
4702 exec_outcome: outcome,
4703 };
4704 Ok((src_delta, scene_graph_delta))
4705 }
4706
4707 fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4708 let commit_failure = || {
4709 KclErrorWithOutputs::from_error_outcome(
4710 KclError::refactor(format!("Could not update KCL after {operation}.")),
4711 outcome.clone(),
4712 )
4713 };
4714
4715 let default_length_unit = self.default_length_unit();
4716 let mut settled_ast = self.program.ast.clone();
4717 let mut committed_solver_value = false;
4718 for (var_range, node_path, value) in &outcome.var_solutions {
4719 let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4720 return Err(commit_failure());
4721 };
4722 let new_value = match &lookup {
4723 Some(current_literal) => {
4724 if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4725 continue;
4726 }
4727 preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4728 }
4729 None => {
4730 Number {
4734 value: number_value_in_default_length_units(*value, default_length_unit),
4735 units: default_length_unit.into(),
4736 }
4737 }
4738 };
4739 committed_solver_value = true;
4740 let source_ref = SourceRef::Simple {
4741 range: *var_range,
4742 node_path: node_path.clone(),
4743 };
4744 mutate_ast_node_by_source_ref(
4745 &mut settled_ast,
4746 &source_ref,
4747 AstMutateCommand::EditVarInitialValue { value: new_value },
4748 )
4749 .map_err(|_| commit_failure())?;
4750 }
4751
4752 if !committed_solver_value {
4753 return Ok(SourceDelta {
4754 text: self.program.original_file_contents.clone(),
4755 });
4756 }
4757
4758 let settled_source = source_from_ast(&settled_ast);
4759 let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4760 if !errors.is_empty() {
4761 return Err(commit_failure());
4762 }
4763 let Some(settled_program) = settled_program else {
4764 return Err(commit_failure());
4765 };
4766
4767 self.program = settled_program;
4768
4769 Ok(SourceDelta { text: settled_source })
4770 }
4771
4772 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4774 if segment_ids_set.contains(&segment_id) {
4775 return true;
4776 }
4777
4778 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4779 return false;
4780 };
4781 let ObjectKind::Segment { segment } = &segment_object.kind else {
4782 return false;
4783 };
4784 let Segment::Point(point) = segment else {
4785 return false;
4786 };
4787
4788 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4789 }
4790
4791 fn remaining_constraint_segments(
4792 &self,
4793 segments: &[ConstraintSegment],
4794 segment_ids_set: &AhashIndexSet<ObjectId>,
4795 ) -> Vec<ConstraintSegment> {
4796 segments
4797 .iter()
4798 .copied()
4799 .filter(|segment| match segment {
4800 ConstraintSegment::Origin(_) => true,
4801 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4802 })
4803 .collect()
4804 }
4805
4806 fn find_referenced_constraints(
4807 &self,
4808 sketch_id: ObjectId,
4809 segment_ids_set: &AhashIndexSet<ObjectId>,
4810 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4811 let sketch_object = self
4813 .scene_graph
4814 .objects
4815 .get(sketch_id.0)
4816 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4817 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4818 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4819 };
4820 let segment_or_owner_matches = |segment_id: ObjectId| {
4821 if segment_ids_set.contains(&segment_id) {
4822 return true;
4823 }
4824 let segment_object = self.scene_graph.objects.get(segment_id.0);
4825 if let Some(obj) = segment_object
4826 && let ObjectKind::Segment { segment } = &obj.kind
4827 {
4828 match segment {
4829 Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4830 Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4831 _ => false,
4832 }
4833 } else {
4834 false
4835 }
4836 };
4837 let mut constraint_ids_set = AhashIndexSet::default();
4838 for constraint_id in &sketch.constraints {
4839 let constraint_object = self
4840 .scene_graph
4841 .objects
4842 .get(constraint_id.0)
4843 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4844 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4845 return Err(KclError::refactor(format!(
4846 "Object is not a constraint, it is {}",
4847 constraint_object.kind.human_friendly_kind_with_article()
4848 )));
4849 };
4850 let depends_on_segment = match constraint {
4851 Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4852 Constraint::Distance(d) => d.point_ids().any(segment_or_owner_matches),
4853 Constraint::Fixed(fixed) => fixed
4854 .points
4855 .iter()
4856 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4857 Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4858 Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4859 Constraint::EqualRadius(equal_radius) => {
4860 equal_radius.input.iter().copied().any(segment_or_owner_matches)
4861 }
4862 Constraint::HorizontalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4863 Constraint::VerticalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4864 Constraint::Horizontal(h) => match h {
4865 Horizontal::Line { line } => segment_or_owner_matches(*line),
4866 Horizontal::Points { points } => points.iter().any(|point| match point {
4867 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4868 ConstraintSegment::Origin(_) => false,
4869 }),
4870 },
4871 Constraint::Vertical(v) => match v {
4872 Vertical::Line { line } => segment_or_owner_matches(*line),
4873 Vertical::Points { points } => points.iter().any(|point| match point {
4874 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4875 ConstraintSegment::Origin(_) => false,
4876 }),
4877 },
4878 Constraint::LinesEqualLength(lines_equal_length) => {
4879 lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4880 }
4881 Constraint::Midpoint(midpoint) => {
4882 segment_or_owner_matches(midpoint.segment)
4883 || matches!(
4884 midpoint.point,
4885 ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4886 )
4887 }
4888 Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4889 Constraint::Perpendicular(perpendicular) => {
4890 perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4891 }
4892 Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4893 Constraint::Symmetric(symmetric) => {
4894 segment_or_owner_matches(symmetric.axis)
4895 || symmetric.input.iter().copied().any(segment_or_owner_matches)
4896 }
4897 Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4898 };
4899 if depends_on_segment {
4900 constraint_ids_set.insert(*constraint_id);
4901 }
4902 }
4903 Ok(constraint_ids_set)
4904 }
4905
4906 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4907 let mut outcome = outcome;
4908 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4909
4910 if freedom_analysis_ran {
4911 self.point_freedom_cache.clear();
4914 for new_obj in &new_objects {
4915 if let ObjectKind::Segment {
4916 segment: crate::front::Segment::Point(point),
4917 } = &new_obj.kind
4918 {
4919 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4920 }
4921 }
4922 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4923 self.scene_graph.objects = new_objects;
4925 } else {
4926 for old_obj in &self.scene_graph.objects {
4929 if let ObjectKind::Segment {
4930 segment: crate::front::Segment::Point(point),
4931 } = &old_obj.kind
4932 {
4933 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4934 }
4935 }
4936
4937 let mut updated_objects = Vec::with_capacity(new_objects.len());
4939 for new_obj in new_objects {
4940 let mut obj = new_obj;
4941 if let ObjectKind::Segment {
4942 segment: crate::front::Segment::Point(point),
4943 } = &mut obj.kind
4944 {
4945 let new_freedom = point.freedom;
4946 match new_freedom {
4952 Freedom::Free => {
4953 match self.point_freedom_cache.get(&obj.id).copied() {
4954 Some(Freedom::Conflict) => {
4955 }
4958 Some(Freedom::Fixed) => {
4959 point.freedom = Freedom::Fixed;
4961 }
4962 Some(Freedom::Free) => {
4963 }
4965 None => {
4966 }
4968 }
4969 }
4970 Freedom::Fixed => {
4971 }
4973 Freedom::Conflict => {
4974 }
4976 }
4977 self.point_freedom_cache.insert(obj.id, point.freedom);
4979 }
4980 updated_objects.push(obj);
4981 }
4982
4983 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4984 self.scene_graph.objects = updated_objects;
4985 }
4986 outcome
4987 }
4988
4989 fn mutate_ast(
4990 &mut self,
4991 ast: &mut ast::Node<ast::Program>,
4992 object_id: ObjectId,
4993 command: AstMutateCommand,
4994 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4995 let sketch_object = self
4996 .scene_graph
4997 .objects
4998 .get(object_id.0)
4999 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5000 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5001 }
5002}
5003
5004fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5005 let sketch_object = scene_graph
5007 .objects
5008 .get(sketch_id.0)
5009 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5010 let ObjectKind::Sketch(_) = &sketch_object.kind else {
5011 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5012 };
5013 expect_single_node_ref(sketch_object)
5014}
5015
5016fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5017 match &object.source {
5018 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5019 range: *range,
5020 node_path: node_path.clone(),
5021 }),
5022 SourceRef::BackTrace { ranges } => {
5023 let [range] = ranges.as_slice() else {
5024 return Err(KclError::refactor(format!(
5025 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5026 ranges.len()
5027 )));
5028 };
5029 Ok(AstNodeRef {
5030 range: range.0,
5031 node_path: range.1.clone(),
5032 })
5033 }
5034 }
5035}
5036
5037fn only_sketch_block_from_range(
5040 ast: &mut ast::Node<ast::Program>,
5041 sketch_block_range: SourceRange,
5042 edit_kind: ChangeKind,
5043) -> Result<(), KclError> {
5044 let r1 = sketch_block_range;
5045 let matches_range = |r2: SourceRange| -> bool {
5046 match edit_kind {
5049 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5050 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5052 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5053 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5055 }
5056 };
5057 let mut found = false;
5058 for item in ast.body.iter_mut() {
5059 match item {
5060 ast::BodyItem::ImportStatement(_) => {}
5061 ast::BodyItem::ExpressionStatement(node) => {
5062 if matches_range(SourceRange::from(&*node))
5063 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5064 {
5065 sketch_block.is_being_edited = true;
5066 found = true;
5067 break;
5068 }
5069 }
5070 ast::BodyItem::VariableDeclaration(node) => {
5071 if matches_range(SourceRange::from(&node.declaration.init))
5072 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5073 {
5074 sketch_block.is_being_edited = true;
5075 found = true;
5076 break;
5077 }
5078 }
5079 ast::BodyItem::TypeDeclaration(_) => {}
5080 ast::BodyItem::ReturnStatement(node) => {
5081 if matches_range(SourceRange::from(&node.argument))
5082 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5083 {
5084 sketch_block.is_being_edited = true;
5085 found = true;
5086 break;
5087 }
5088 }
5089 }
5090 }
5091 if !found {
5092 return Err(KclError::refactor(format!(
5093 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5094 )));
5095 }
5096
5097 Ok(())
5098}
5099
5100fn only_sketch_block(
5101 ast: &mut ast::Node<ast::Program>,
5102 sketch_block_ref: &AstNodeRef,
5103 edit_kind: ChangeKind,
5104) -> Result<(), KclError> {
5105 let Some(target_node_path) = &sketch_block_ref.node_path else {
5106 #[cfg(target_arch = "wasm32")]
5107 web_sys::console::warn_1(
5108 &format!(
5109 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5110 &sketch_block_ref
5111 )
5112 .into(),
5113 );
5114 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5115 };
5116 let mut found = false;
5117 for item in ast.body.iter_mut() {
5118 match item {
5119 ast::BodyItem::ImportStatement(_) => {}
5120 ast::BodyItem::ExpressionStatement(node) => {
5121 if let Some(node_path) = &node.node_path
5123 && node_path == target_node_path
5124 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5125 {
5126 sketch_block.is_being_edited = true;
5127 found = true;
5128 break;
5129 }
5130 if let Some(node_path) = node.expression.node_path()
5132 && node_path == target_node_path
5133 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5134 {
5135 sketch_block.is_being_edited = true;
5136 found = true;
5137 break;
5138 }
5139 }
5140 ast::BodyItem::VariableDeclaration(node) => {
5141 if let Some(node_path) = node.declaration.init.node_path()
5142 && node_path == target_node_path
5143 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5144 {
5145 sketch_block.is_being_edited = true;
5146 found = true;
5147 break;
5148 }
5149 }
5150 ast::BodyItem::TypeDeclaration(_) => {}
5151 ast::BodyItem::ReturnStatement(node) => {
5152 if let Some(node_path) = node.argument.node_path()
5153 && node_path == target_node_path
5154 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5155 {
5156 sketch_block.is_being_edited = true;
5157 found = true;
5158 break;
5159 }
5160 }
5161 }
5162 }
5163 if !found {
5164 return Err(KclError::refactor(format!(
5165 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5166 )));
5167 }
5168
5169 Ok(())
5170}
5171
5172fn sketch_on_ast_expr(
5173 ast: &mut ast::Node<ast::Program>,
5174 scene_graph: &SceneGraph,
5175 on: &Plane,
5176) -> Result<ast::Expr, KclError> {
5177 match on {
5178 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5179 Plane::Object(object_id) => {
5180 let on_object = scene_graph
5181 .objects
5182 .get(object_id.0)
5183 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5184 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5185 return Ok(face_expr);
5186 }
5187 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5188 }
5189 }
5190}
5191
5192fn sketch_face_of_scene_object_ast_expr(
5193 ast: &mut ast::Node<ast::Program>,
5194 on_object: &crate::front::Object,
5195) -> Result<Option<ast::Expr>, KclError> {
5196 let SourceRef::BackTrace { ranges } = &on_object.source else {
5197 return Ok(None);
5198 };
5199
5200 match &on_object.kind {
5201 ObjectKind::Wall(_) => {
5202 let [sweep_range, segment_range] = ranges.as_slice() else {
5203 return Err(KclError::refactor(format!(
5204 "Expected wall source metadata to have 2 ranges, got {}; artifact_id={:?}",
5205 ranges.len(),
5206 on_object.artifact_id
5207 )));
5208 };
5209 let sweep_ref = get_or_insert_ast_reference(
5210 ast,
5211 &SourceRef::Simple {
5212 range: sweep_range.0,
5213 node_path: sweep_range.1.clone(),
5214 },
5215 "solid",
5216 None,
5217 )?;
5218 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
5219 return Err(KclError::refactor(format!(
5220 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5221 on_object.artifact_id
5222 )));
5223 };
5224 let solid_name = solid_name_expr.name.name.clone();
5225 let solid_expr = ast_name_expr(solid_name.clone());
5226 let segment_ref = get_or_insert_ast_reference(
5227 ast,
5228 &SourceRef::Simple {
5229 range: segment_range.0,
5230 node_path: segment_range.1.clone(),
5231 },
5232 LINE_VARIABLE,
5233 None,
5234 )?;
5235
5236 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &solid_name) {
5237 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5238 return Err(KclError::refactor(format!(
5239 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5240 on_object.artifact_id
5241 )));
5242 };
5243 create_member_expression(
5244 create_member_expression(ast_name_expr(region_name), "tags"),
5245 &segment_name_expr.name.name,
5246 )
5247 } else {
5248 segment_ref
5249 };
5250
5251 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5252 }
5253 ObjectKind::Cap(cap) => {
5254 let [range] = ranges.as_slice() else {
5255 return Err(KclError::refactor(format!(
5256 "Expected cap source metadata to have 1 range, got {}; artifact_id={:?}",
5257 ranges.len(),
5258 on_object.artifact_id
5259 )));
5260 };
5261 let sweep_ref = get_or_insert_ast_reference(
5262 ast,
5263 &SourceRef::Simple {
5264 range: range.0,
5265 node_path: range.1.clone(),
5266 },
5267 "solid",
5268 None,
5269 )?;
5270 let ast::Expr::Name(solid_name_expr) = sweep_ref else {
5271 return Err(KclError::refactor(format!(
5272 "Could not resolve sweep reference for selected cap: artifact_id={:?}",
5273 on_object.artifact_id
5274 )));
5275 };
5276 let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
5277 let face_expr = match cap.kind {
5279 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5280 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5281 };
5282
5283 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5284 }
5285 _ => Ok(None),
5286 }
5287}
5288
5289fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5290 let mut existing_artifact_ids = scene_objects
5291 .iter()
5292 .map(|object| object.artifact_id)
5293 .collect::<HashSet<_>>();
5294
5295 for artifact in artifact_graph.values() {
5296 match artifact {
5297 Artifact::Wall(wall) => {
5298 if existing_artifact_ids.contains(&wall.id) {
5299 continue;
5300 }
5301
5302 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5303 Artifact::Segment(segment) => Some(segment),
5304 _ => None,
5305 }) else {
5306 continue;
5307 };
5308 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5309 Artifact::Sweep(sweep) => Some(sweep),
5310 _ => None,
5311 }) else {
5312 continue;
5313 };
5314 let source_segment = segment
5315 .original_seg_id
5316 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5317 .and_then(|artifact| match artifact {
5318 Artifact::Segment(segment) => Some(segment),
5319 _ => None,
5320 })
5321 .unwrap_or(segment);
5322 let id = ObjectId(scene_objects.len());
5323 scene_objects.push(crate::front::Object {
5324 id,
5325 kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
5326 label: Default::default(),
5327 comments: Default::default(),
5328 artifact_id: wall.id,
5329 source: SourceRef::BackTrace {
5330 ranges: vec![
5331 (sweep.code_ref.range, Some(sweep.code_ref.node_path.clone())),
5332 (
5333 source_segment.code_ref.range,
5334 Some(source_segment.code_ref.node_path.clone()),
5335 ),
5336 ],
5337 },
5338 });
5339 existing_artifact_ids.insert(wall.id);
5340 }
5341 Artifact::Cap(cap) => {
5342 if existing_artifact_ids.contains(&cap.id) {
5343 continue;
5344 }
5345
5346 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5347 Artifact::Sweep(sweep) => Some(sweep),
5348 _ => None,
5349 }) else {
5350 continue;
5351 };
5352 let id = ObjectId(scene_objects.len());
5353 let kind = match cap.sub_type {
5354 CapSubType::Start => crate::frontend::api::CapKind::Start,
5355 CapSubType::End => crate::frontend::api::CapKind::End,
5356 };
5357 scene_objects.push(crate::front::Object {
5358 id,
5359 kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
5360 label: Default::default(),
5361 comments: Default::default(),
5362 artifact_id: cap.id,
5363 source: SourceRef::BackTrace {
5364 ranges: vec![(sweep.code_ref.range, Some(sweep.code_ref.node_path.clone()))],
5365 },
5366 });
5367 existing_artifact_ids.insert(cap.id);
5368 }
5369 _ => {}
5370 }
5371 }
5372}
5373
5374fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5375 use crate::engine::PlaneName;
5376
5377 match name {
5378 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5379 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5380 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5381 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5382 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5383 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5384 }
5385}
5386
5387fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5388 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5389 ast::UnaryOperator::Neg,
5390 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5391 )))
5392}
5393
5394fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5395 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5396 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5397 unlabeled: Some(solid_expr),
5398 arguments: vec![ast::LabeledArg {
5399 label: Some(ast::Identifier::new("face")),
5400 arg: face_expr,
5401 }],
5402 digest: None,
5403 non_code_meta: Default::default(),
5404 })))
5405}
5406
5407fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5408 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5409 return None;
5410 };
5411 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5412 return None;
5413 };
5414 if !matches!(
5415 sweep_call.callee.name.name.as_str(),
5416 "extrude" | "revolve" | "sweep" | "loft"
5417 ) {
5418 return None;
5419 }
5420 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5421 return None;
5422 };
5423 let candidate = region_name_expr.name.name.clone();
5424 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5425 return None;
5426 };
5427 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5428 return None;
5429 };
5430 if region_call.callee.name.name != "region" {
5431 return None;
5432 }
5433 Some(candidate)
5434}
5435
5436fn get_or_insert_ast_reference(
5443 ast: &mut ast::Node<ast::Program>,
5444 source_ref: &SourceRef,
5445 prefix: &str,
5446 property: Option<&str>,
5447) -> Result<ast::Expr, KclError> {
5448 let command = AstMutateCommand::AddVariableDeclaration {
5449 prefix: prefix.to_owned(),
5450 };
5451 let (_, ret) = mutate_ast_node_by_source_ref(ast, source_ref, command)?;
5452 let AstMutateCommandReturn::Name(var_name) = ret else {
5453 return Err(KclError::refactor(
5454 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5455 ));
5456 };
5457 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5458 let Some(property) = property else {
5459 return Ok(var_expr);
5461 };
5462
5463 Ok(create_member_expression(var_expr, property))
5464}
5465
5466fn mutate_ast_node_by_source_ref(
5467 ast: &mut ast::Node<ast::Program>,
5468 source_ref: &SourceRef,
5469 command: AstMutateCommand,
5470) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5471 let (source_range, node_path) = match source_ref {
5472 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5473 SourceRef::BackTrace { ranges } => {
5474 let [range] = ranges.as_slice() else {
5475 return Err(KclError::refactor(format!(
5476 "Expected single source ref, got {}; ranges={ranges:#?}",
5477 ranges.len(),
5478 )));
5479 };
5480 (range.0, range.1.clone())
5481 }
5482 };
5483 let mut context = AstMutateContext {
5484 source_range,
5485 node_path,
5486 command,
5487 defined_names_stack: Default::default(),
5488 };
5489 let control = dfs_mut(ast, &mut context);
5490 match control {
5491 ControlFlow::Continue(_) => Err(KclError::refactor(
5492 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5493 )),
5494 ControlFlow::Break(break_value) => break_value,
5495 }
5496}
5497
5498#[derive(Debug)]
5499struct AstMutateContext {
5500 source_range: SourceRange,
5501 node_path: Option<ast::NodePath>,
5502 command: AstMutateCommand,
5503 defined_names_stack: Vec<HashSet<String>>,
5504}
5505
5506#[derive(Debug)]
5507#[allow(clippy::large_enum_variant)]
5508enum AstMutateCommand {
5509 AddSketchBlockExprStmt {
5511 expr: ast::Expr,
5512 },
5513 AddSketchBlockVarDecl {
5515 prefix: String,
5516 expr: ast::Expr,
5517 },
5518 AddVariableDeclaration {
5519 prefix: String,
5520 },
5521 EditPoint {
5522 at: ast::Expr,
5523 },
5524 EditLine {
5525 start: ast::Expr,
5526 end: ast::Expr,
5527 construction: Option<bool>,
5528 },
5529 EditArc {
5530 start: ast::Expr,
5531 end: ast::Expr,
5532 center: ast::Expr,
5533 construction: Option<bool>,
5534 },
5535 EditCircle {
5536 start: ast::Expr,
5537 center: ast::Expr,
5538 construction: Option<bool>,
5539 },
5540 EditControlPointSpline {
5541 points: ast::Expr,
5542 construction: Option<bool>,
5543 },
5544 EditConstraintValue {
5545 value: ast::BinaryPart,
5546 },
5547 EditDistanceConstraintLabelPosition {
5548 label_position: ast::Expr,
5549 },
5550 EditCallUnlabeled {
5551 arg: ast::Expr,
5552 },
5553 EditVarInitialValue {
5554 value: Number,
5555 },
5556 DeleteNode,
5557}
5558
5559impl AstMutateCommand {
5560 fn needs_defined_names_stack(&self) -> bool {
5561 matches!(
5562 self,
5563 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5564 )
5565 }
5566}
5567
5568#[derive(Debug)]
5569enum AstMutateCommandReturn {
5570 None,
5571 Name(String),
5572}
5573
5574#[derive(Debug, Clone)]
5575struct AstNodeRef {
5576 range: SourceRange,
5577 node_path: Option<ast::NodePath>,
5578}
5579
5580impl<T> From<&ast::Node<T>> for AstNodeRef {
5581 fn from(value: &ast::Node<T>) -> Self {
5582 AstNodeRef {
5583 range: value.into(),
5584 node_path: value.node_path.clone(),
5585 }
5586 }
5587}
5588
5589impl From<&ast::BodyItem> for AstNodeRef {
5590 fn from(value: &ast::BodyItem) -> Self {
5591 match value {
5592 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5593 range: node.into(),
5594 node_path: node.node_path.clone(),
5595 },
5596 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5597 range: node.into(),
5598 node_path: node.node_path.clone(),
5599 },
5600 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5601 range: node.into(),
5602 node_path: node.node_path.clone(),
5603 },
5604 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5605 range: node.into(),
5606 node_path: node.node_path.clone(),
5607 },
5608 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5609 range: node.into(),
5610 node_path: node.node_path.clone(),
5611 },
5612 }
5613 }
5614}
5615
5616impl From<&ast::Expr> for AstNodeRef {
5617 fn from(value: &ast::Expr) -> Self {
5618 AstNodeRef {
5619 range: SourceRange::from(value),
5620 node_path: value.node_path().cloned(),
5621 }
5622 }
5623}
5624
5625impl From<&AstMutateContext> for AstNodeRef {
5626 fn from(value: &AstMutateContext) -> Self {
5627 AstNodeRef {
5628 range: value.source_range,
5629 node_path: value.node_path.clone(),
5630 }
5631 }
5632}
5633
5634impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5635 type Error = crate::walk::AstNodeError;
5636
5637 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5638 Ok(AstNodeRef {
5639 range: SourceRange::try_from(value)?,
5640 node_path: value.try_into()?,
5641 })
5642 }
5643}
5644
5645impl From<AstNodeRef> for SourceRange {
5646 fn from(value: AstNodeRef) -> Self {
5647 value.range
5648 }
5649}
5650
5651impl Visitor for AstMutateContext {
5652 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5653 type Continue = ();
5654
5655 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5656 filter_and_process(self, node)
5657 }
5658
5659 fn finish(&mut self, node: NodeMut<'_>) {
5660 match &node {
5661 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5662 self.defined_names_stack.pop();
5663 }
5664 _ => {}
5665 }
5666 }
5667}
5668
5669fn filter_and_process(
5670 ctx: &mut AstMutateContext,
5671 node: NodeMut,
5672) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5673 let Ok(node_range) = SourceRange::try_from(&node) else {
5674 return TraversalReturn::new_continue(());
5676 };
5677 if let NodeMut::VariableDeclaration(var_decl) = &node {
5682 let expr_range = SourceRange::from(&var_decl.declaration.init);
5683 let expr_node_path = var_decl.declaration.init.node_path();
5684 if source_ref_matches(ctx, expr_range, expr_node_path) {
5685 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5686 return TraversalReturn::new_break(Ok((
5689 AstNodeRef::from(&**var_decl),
5690 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5691 )));
5692 }
5693 if let AstMutateCommand::DeleteNode = &ctx.command {
5694 return TraversalReturn {
5697 mutate_body_item: MutateBodyItem::Delete,
5698 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5699 };
5700 }
5701 }
5702 }
5703 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5706 let expr_range = SourceRange::from(&expr_stmt.expression);
5707 let expr_node_path = expr_stmt.expression.node_path();
5708 if source_ref_matches(ctx, expr_range, expr_node_path) {
5709 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5710 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5713 return TraversalReturn::new_continue(());
5714 };
5715 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5716 }
5717 if let AstMutateCommand::DeleteNode = &ctx.command {
5718 return TraversalReturn {
5721 mutate_body_item: MutateBodyItem::Delete,
5722 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5723 };
5724 }
5725 }
5726 }
5727
5728 if ctx.command.needs_defined_names_stack() {
5729 if let NodeMut::Program(program) = &node {
5730 ctx.defined_names_stack.push(find_defined_names(*program));
5731 } else if let NodeMut::SketchBlock(block) = &node {
5732 ctx.defined_names_stack.push(find_defined_names(&block.body));
5733 }
5734 }
5735
5736 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5738 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5739 return TraversalReturn::new_continue(());
5740 }
5741 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5742 return TraversalReturn::new_continue(());
5743 };
5744 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5745}
5746
5747fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5748 match &ctx.node_path {
5749 Some(target) => Some(target) == node_path,
5750 None => node_range == ctx.source_range,
5751 }
5752}
5753
5754fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5755 match &ctx.command {
5756 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5757 if let NodeMut::SketchBlock(sketch_block) = node {
5758 sketch_block
5759 .body
5760 .items
5761 .push(ast::BodyItem::ExpressionStatement(ast::Node {
5762 inner: ast::ExpressionStatement {
5763 expression: expr.clone(),
5764 digest: None,
5765 },
5766 start: Default::default(),
5767 end: Default::default(),
5768 module_id: Default::default(),
5769 node_path: None,
5770 outer_attrs: Default::default(),
5771 pre_comments: Default::default(),
5772 comment_start: Default::default(),
5773 }));
5774 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5775 }
5776 }
5777 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5778 if let NodeMut::SketchBlock(sketch_block) = node {
5779 let empty_defined_names = HashSet::new();
5780 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5781 let Ok(name) = next_free_name(prefix, defined_names) else {
5782 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5783 };
5784 sketch_block
5785 .body
5786 .items
5787 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5788 ast::VariableDeclaration::new(
5789 ast::VariableDeclarator::new(&name, expr.clone()),
5790 ast::ItemVisibility::Default,
5791 ast::VariableKind::Const,
5792 ),
5793 ))));
5794 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5795 }
5796 }
5797 AstMutateCommand::AddVariableDeclaration { prefix } => {
5798 if let NodeMut::VariableDeclaration(inner) = node {
5799 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5800 }
5801 if let NodeMut::ExpressionStatement(expr_stmt) = node {
5802 let empty_defined_names = HashSet::new();
5803 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5804 let Ok(name) = next_free_name(prefix, defined_names) else {
5805 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5807 };
5808 let mutate_node =
5809 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5810 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5811 ast::ItemVisibility::Default,
5812 ast::VariableKind::Const,
5813 ))));
5814 return TraversalReturn {
5815 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5816 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5817 };
5818 }
5819 }
5820 AstMutateCommand::EditPoint { at } => {
5821 if let NodeMut::CallExpressionKw(call) = node {
5822 if call.callee.name.name != POINT_FN {
5823 return TraversalReturn::new_continue(());
5824 }
5825 for labeled_arg in &mut call.arguments {
5827 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5828 labeled_arg.arg = at.clone();
5829 }
5830 }
5831 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5832 }
5833 }
5834 AstMutateCommand::EditLine {
5835 start,
5836 end,
5837 construction,
5838 } => {
5839 if let NodeMut::CallExpressionKw(call) = node {
5840 if call.callee.name.name != LINE_FN {
5841 return TraversalReturn::new_continue(());
5842 }
5843 for labeled_arg in &mut call.arguments {
5845 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5846 labeled_arg.arg = start.clone();
5847 }
5848 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5849 labeled_arg.arg = end.clone();
5850 }
5851 }
5852 if let Some(construction_value) = construction {
5854 let construction_exists = call
5855 .arguments
5856 .iter()
5857 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5858 if *construction_value {
5859 if construction_exists {
5861 for labeled_arg in &mut call.arguments {
5863 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5864 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5865 value: ast::LiteralValue::Bool(true),
5866 raw: "true".to_string(),
5867 digest: None,
5868 })));
5869 }
5870 }
5871 } else {
5872 call.arguments.push(ast::LabeledArg {
5874 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5875 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5876 value: ast::LiteralValue::Bool(true),
5877 raw: "true".to_string(),
5878 digest: None,
5879 }))),
5880 });
5881 }
5882 } else {
5883 call.arguments
5885 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5886 }
5887 }
5888 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5889 }
5890 }
5891 AstMutateCommand::EditArc {
5892 start,
5893 end,
5894 center,
5895 construction,
5896 } => {
5897 if let NodeMut::CallExpressionKw(call) = node {
5898 if call.callee.name.name != ARC_FN {
5899 return TraversalReturn::new_continue(());
5900 }
5901 for labeled_arg in &mut call.arguments {
5903 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
5904 labeled_arg.arg = start.clone();
5905 }
5906 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
5907 labeled_arg.arg = end.clone();
5908 }
5909 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
5910 labeled_arg.arg = center.clone();
5911 }
5912 }
5913 if let Some(construction_value) = construction {
5915 let construction_exists = call
5916 .arguments
5917 .iter()
5918 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5919 if *construction_value {
5920 if construction_exists {
5922 for labeled_arg in &mut call.arguments {
5924 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5925 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5926 value: ast::LiteralValue::Bool(true),
5927 raw: "true".to_string(),
5928 digest: None,
5929 })));
5930 }
5931 }
5932 } else {
5933 call.arguments.push(ast::LabeledArg {
5935 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5936 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5937 value: ast::LiteralValue::Bool(true),
5938 raw: "true".to_string(),
5939 digest: None,
5940 }))),
5941 });
5942 }
5943 } else {
5944 call.arguments
5946 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
5947 }
5948 }
5949 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5950 }
5951 }
5952 AstMutateCommand::EditCircle {
5953 start,
5954 center,
5955 construction,
5956 } => {
5957 if let NodeMut::CallExpressionKw(call) = node {
5958 if call.callee.name.name != CIRCLE_FN {
5959 return TraversalReturn::new_continue(());
5960 }
5961 for labeled_arg in &mut call.arguments {
5963 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
5964 labeled_arg.arg = start.clone();
5965 }
5966 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
5967 labeled_arg.arg = center.clone();
5968 }
5969 }
5970 if let Some(construction_value) = construction {
5972 let construction_exists = call
5973 .arguments
5974 .iter()
5975 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
5976 if *construction_value {
5977 if construction_exists {
5978 for labeled_arg in &mut call.arguments {
5980 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
5981 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5982 value: ast::LiteralValue::Bool(true),
5983 raw: "true".to_string(),
5984 digest: None,
5985 })));
5986 }
5987 }
5988 } else {
5989 call.arguments.push(ast::LabeledArg {
5991 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
5992 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
5993 value: ast::LiteralValue::Bool(true),
5994 raw: "true".to_string(),
5995 digest: None,
5996 }))),
5997 });
5998 }
5999 } else {
6000 call.arguments
6002 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6003 }
6004 }
6005 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6006 }
6007 }
6008 AstMutateCommand::EditControlPointSpline { points, construction } => {
6009 if let NodeMut::CallExpressionKw(call) = node {
6010 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6011 return TraversalReturn::new_continue(());
6012 }
6013 for labeled_arg in &mut call.arguments {
6014 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6015 {
6016 labeled_arg.arg = points.clone();
6017 }
6018 }
6019 if let Some(construction_value) = construction {
6021 let construction_exists = call
6022 .arguments
6023 .iter()
6024 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6025 if *construction_value {
6026 if construction_exists {
6027 for labeled_arg in &mut call.arguments {
6028 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6029 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6030 value: ast::LiteralValue::Bool(true),
6031 raw: "true".to_string(),
6032 digest: None,
6033 })));
6034 }
6035 }
6036 } else {
6037 call.arguments.push(ast::LabeledArg {
6038 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6039 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6040 value: ast::LiteralValue::Bool(true),
6041 raw: "true".to_string(),
6042 digest: None,
6043 }))),
6044 });
6045 }
6046 } else {
6047 call.arguments
6048 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6049 }
6050 }
6051 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6052 }
6053 }
6054 AstMutateCommand::EditConstraintValue { value } => {
6055 if let NodeMut::BinaryExpression(binary_expr) = node {
6056 let left_is_constraint = matches!(
6057 &binary_expr.left,
6058 ast::BinaryPart::CallExpressionKw(call)
6059 if matches!(
6060 call.callee.name.name.as_str(),
6061 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
6062 )
6063 );
6064 if left_is_constraint {
6065 binary_expr.right = value.clone();
6066 } else {
6067 binary_expr.left = value.clone();
6068 }
6069
6070 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6071 }
6072 }
6073 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6074 if let NodeMut::BinaryExpression(binary_expr) = node {
6075 let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
6076 return TraversalReturn::new_continue(());
6077 };
6078 if !matches!(
6079 call.callee.name.name.as_str(),
6080 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
6081 ) {
6082 return TraversalReturn::new_continue(());
6083 }
6084
6085 if let Some(label_arg) = call
6086 .arguments
6087 .iter_mut()
6088 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6089 {
6090 label_arg.arg = label_position.clone();
6091 } else {
6092 call.arguments.push(ast::LabeledArg {
6093 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6094 arg: label_position.clone(),
6095 });
6096 }
6097
6098 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6099 }
6100 }
6101 AstMutateCommand::EditCallUnlabeled { arg } => {
6102 if let NodeMut::CallExpressionKw(call) = node {
6103 call.unlabeled = Some(arg.clone());
6104 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6105 }
6106 }
6107 AstMutateCommand::EditVarInitialValue { value } => {
6108 if let NodeMut::SketchVar(sketch_var) = node {
6112 let Ok(literal) = to_source_number(*value) else {
6113 return TraversalReturn::new_break(Err(KclError::refactor(format!(
6114 "Could not convert number to AST literal: {:?}",
6115 *value
6116 ))));
6117 };
6118 sketch_var.initial = Some(Box::new(ast::Node::no_src(literal)));
6119 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6120 }
6121 }
6122 AstMutateCommand::DeleteNode => {
6123 return TraversalReturn {
6124 mutate_body_item: MutateBodyItem::Delete,
6125 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6126 };
6127 }
6128 }
6129 TraversalReturn::new_continue(())
6130}
6131
6132struct FindSketchBlockSourceRange {
6133 target_before_mutation: SourceRange,
6135 found: Cell<Option<AstNodeRef>>,
6139}
6140
6141impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6142 type Error = crate::front::Error;
6143
6144 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6145 let Ok(node_range) = SourceRange::try_from(&node) else {
6146 return Ok(true);
6147 };
6148
6149 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6150 if node_range.module_id() == self.target_before_mutation.module_id()
6151 && node_range.start() == self.target_before_mutation.start()
6152 && node_range.end() >= self.target_before_mutation.end()
6154 {
6155 self.found.set(sketch_block.body.items.last().map(|item| match item {
6156 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6160 _ => AstNodeRef::from(item),
6161 }));
6162 return Ok(false);
6163 } else {
6164 return Ok(true);
6167 }
6168 }
6169
6170 for child in node.children().iter() {
6171 if !child.visit(*self)? {
6172 return Ok(false);
6173 }
6174 }
6175
6176 Ok(true)
6177 }
6178}
6179
6180struct FindSketchBlockByNodePath {
6181 target_node_path: ast::NodePath,
6183 found: Cell<Option<AstNodeRef>>,
6187}
6188
6189impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6190 type Error = crate::front::Error;
6191
6192 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6193 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6194 return Ok(true);
6195 };
6196
6197 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6198 if let Some(node_path) = node_path
6199 && node_path == self.target_node_path
6200 {
6201 self.found.set(sketch_block.body.items.last().map(|item| match item {
6202 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6206 _ => AstNodeRef::from(item),
6207 }));
6208
6209 return Ok(false);
6210 } else {
6211 return Ok(true);
6214 }
6215 }
6216
6217 for child in node.children().iter() {
6218 if !child.visit(*self)? {
6219 return Ok(false);
6220 }
6221 }
6222
6223 Ok(true)
6224 }
6225}
6226
6227fn find_sketch_block_added_item(
6235 ast: &ast::Node<ast::Program>,
6236 sketch_block_before_mutation: &AstNodeRef,
6237) -> Result<AstNodeRef, KclError> {
6238 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6239 let find = FindSketchBlockByNodePath {
6240 target_node_path: node_path.clone(),
6241 found: Cell::new(None),
6242 };
6243 let node = crate::walk::Node::from(ast);
6244 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6245 find.found.into_inner().ok_or_else(|| {
6246 KclError::refactor(format!(
6247 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6248 ))
6249 })
6250 } else {
6251 let find = FindSketchBlockSourceRange {
6253 target_before_mutation: sketch_block_before_mutation.range,
6254 found: Cell::new(None),
6255 };
6256 let node = crate::walk::Node::from(ast);
6257 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6258 find.found.into_inner().ok_or_else(|| KclError::refactor(
6259 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?"),
6260 ))
6261 }
6262}
6263
6264fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6265 ast.recast_top(&Default::default(), 0)
6267}
6268
6269struct FindNumericLiteral {
6270 target: SourceRange,
6271 found: Cell<Option<ast::NumericLiteral>>,
6272}
6273
6274impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6275 type Error = crate::front::Error;
6276
6277 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6278 let Ok(node_range) = SourceRange::try_from(&node) else {
6279 return Ok(true);
6280 };
6281
6282 if node_range == self.target
6283 && let crate::walk::Node::NumericLiteral(literal) = node
6284 {
6285 self.found.set(Some(literal.inner.clone()));
6286 return Ok(false);
6287 }
6288
6289 for child in node.children().iter() {
6290 if !child.visit(*self)? {
6291 return Ok(false);
6292 }
6293 }
6294
6295 Ok(true)
6296 }
6297}
6298
6299fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6300 let find = FindNumericLiteral {
6301 target,
6302 found: Cell::new(None),
6303 };
6304 let node = crate::walk::Node::from(ast);
6305 node.visit(&find).ok()?;
6306 find.found.into_inner()
6307}
6308
6309struct FindSketchVarInitialByNodePath<'a> {
6310 target: &'a ast::NodePath,
6311 sketch_var_found: Cell<bool>,
6312 initial_literal: Cell<Option<ast::NumericLiteral>>,
6313}
6314
6315impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6316 type Error = crate::front::Error;
6317
6318 fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6319 if let crate::walk::Node::SketchVar(sketch_var) = node
6320 && sketch_var.node_path.as_ref() == Some(self.target)
6321 {
6322 self.sketch_var_found.set(true);
6323 if let Some(initial) = &sketch_var.initial {
6324 self.initial_literal.set(Some(initial.inner.clone()));
6325 }
6326 return Ok(false);
6327 }
6328
6329 for child in node.children().iter() {
6330 if !child.visit(*self)? {
6331 return Ok(false);
6332 }
6333 }
6334
6335 Ok(true)
6336 }
6337}
6338
6339fn numeric_literal_at_node_path(
6349 ast: &ast::Node<ast::Program>,
6350 node_path: Option<&ast::NodePath>,
6351 source_range: SourceRange,
6352) -> Option<Option<ast::NumericLiteral>> {
6353 let Some(node_path) = node_path else {
6354 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";
6355 #[cfg(target_arch = "wasm32")]
6356 web_sys::console::warn_1(&message.into());
6357 #[cfg(not(target_arch = "wasm32"))]
6358 eprintln!("WARNING: {message}");
6359 return numeric_literal_at_source_range(ast, source_range).map(Some);
6360 };
6361 let find = FindSketchVarInitialByNodePath {
6362 target: node_path,
6363 sketch_var_found: Cell::new(false),
6364 initial_literal: Cell::new(None),
6365 };
6366 let node = crate::walk::Node::from(ast);
6367 node.visit(&find).ok()?;
6368 if !find.sketch_var_found.get() {
6369 return None;
6370 }
6371 Some(find.initial_literal.into_inner())
6372}
6373
6374fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6375 match suffix {
6376 NumericSuffix::Mm => Some(UnitLength::Millimeters),
6377 NumericSuffix::Cm => Some(UnitLength::Centimeters),
6378 NumericSuffix::M => Some(UnitLength::Meters),
6379 NumericSuffix::Inch => Some(UnitLength::Inches),
6380 NumericSuffix::Ft => Some(UnitLength::Feet),
6381 NumericSuffix::Yd => Some(UnitLength::Yards),
6382 _ => None,
6383 }
6384}
6385
6386fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6387 match suffix_length_unit(number.units) {
6388 Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6389 None => number.value,
6390 }
6391}
6392
6393fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6394 match suffix_length_unit(literal.suffix) {
6395 Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6396 None => literal.value,
6397 }
6398}
6399
6400fn var_solution_needs_commit(
6401 current_literal: &ast::NumericLiteral,
6402 solved_value: Number,
6403 default_length_unit: UnitLength,
6404) -> bool {
6405 let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6406 let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6407
6408 (current - solved).abs() > 1e-9
6409}
6410
6411fn preserve_var_solution_literal_style(
6412 current_literal: &ast::NumericLiteral,
6413 solved_value: Number,
6414 default_length_unit: UnitLength,
6415) -> Number {
6416 if current_literal.suffix == NumericSuffix::None {
6417 return Number {
6418 value: number_value_in_default_length_units(solved_value, default_length_unit),
6419 units: NumericSuffix::None,
6420 };
6421 }
6422
6423 let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6424 return solved_value;
6425 };
6426
6427 let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6428 Number {
6429 value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6430 units: current_literal.suffix,
6431 }
6432}
6433
6434pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6435 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
6436 inner: ast::ArrayExpression {
6437 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6438 non_code_meta: Default::default(),
6439 digest: None,
6440 },
6441 start: Default::default(),
6442 end: Default::default(),
6443 module_id: Default::default(),
6444 node_path: None,
6445 outer_attrs: Default::default(),
6446 pre_comments: Default::default(),
6447 comment_start: Default::default(),
6448 })))
6449}
6450
6451pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6452 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6453 ast::ArrayExpression {
6454 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6455 digest: None,
6456 non_code_meta: Default::default(),
6457 },
6458 ))))
6459}
6460
6461fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6462 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6463 ast::ArrayExpression {
6464 elements: vec![
6465 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6466 point.x,
6467 )?)))),
6468 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6469 point.y,
6470 )?)))),
6471 ],
6472 non_code_meta: Default::default(),
6473 digest: None,
6474 },
6475 ))))
6476}
6477
6478fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6479 match expr {
6480 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
6481 inner: ast::Literal::from(to_source_number(*number)?),
6482 start: Default::default(),
6483 end: Default::default(),
6484 module_id: Default::default(),
6485 node_path: None,
6486 outer_attrs: Default::default(),
6487 pre_comments: Default::default(),
6488 comment_start: Default::default(),
6489 }))),
6490 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
6491 inner: ast::SketchVar {
6492 initial: Some(Box::new(ast::Node {
6493 inner: to_source_number(*number)?,
6494 start: Default::default(),
6495 end: Default::default(),
6496 module_id: Default::default(),
6497 node_path: None,
6498 outer_attrs: Default::default(),
6499 pre_comments: Default::default(),
6500 comment_start: Default::default(),
6501 })),
6502 digest: None,
6503 },
6504 start: Default::default(),
6505 end: Default::default(),
6506 module_id: Default::default(),
6507 node_path: None,
6508 outer_attrs: Default::default(),
6509 pre_comments: Default::default(),
6510 comment_start: Default::default(),
6511 }))),
6512 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6513 }
6514}
6515
6516fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6517 Ok(ast::NumericLiteral {
6518 value: number.value,
6519 suffix: number.units,
6520 raw: format_number_literal(number.value, number.units, None)?,
6521 digest: None,
6522 })
6523}
6524
6525pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
6526 ast::Expr::Name(Box::new(ast_name(name)))
6527}
6528
6529fn ast_name(name: String) -> ast::Node<ast::Name> {
6530 ast::Node {
6531 inner: ast::Name {
6532 name: ast::Node {
6533 inner: ast::Identifier { name, digest: None },
6534 start: Default::default(),
6535 end: Default::default(),
6536 module_id: Default::default(),
6537 node_path: None,
6538 outer_attrs: Default::default(),
6539 pre_comments: Default::default(),
6540 comment_start: Default::default(),
6541 },
6542 path: Vec::new(),
6543 abs_path: false,
6544 digest: None,
6545 },
6546 start: Default::default(),
6547 end: Default::default(),
6548 module_id: Default::default(),
6549 node_path: None,
6550 outer_attrs: Default::default(),
6551 pre_comments: Default::default(),
6552 comment_start: Default::default(),
6553 }
6554}
6555
6556pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
6557 ast::Name {
6558 name: ast::Node {
6559 inner: ast::Identifier {
6560 name: name.to_owned(),
6561 digest: None,
6562 },
6563 start: Default::default(),
6564 end: Default::default(),
6565 module_id: Default::default(),
6566 node_path: None,
6567 outer_attrs: Default::default(),
6568 pre_comments: Default::default(),
6569 comment_start: Default::default(),
6570 },
6571 path: Default::default(),
6572 abs_path: false,
6573 digest: None,
6574 }
6575}
6576
6577pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6581 let elements = exprs.into_iter().collect::<Vec<_>>();
6582 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6583
6584 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6586 elements,
6587 digest: None,
6588 non_code_meta: Default::default(),
6589 })));
6590
6591 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6593 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6594 unlabeled: Some(array_expr),
6595 arguments: Default::default(),
6596 digest: None,
6597 non_code_meta: Default::default(),
6598 })))
6599}
6600
6601pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6603 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6604 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6605 unlabeled: None,
6606 arguments: vec![
6607 ast::LabeledArg {
6608 label: Some(ast::Identifier::new(LINE_START_PARAM)),
6609 arg: start_ast,
6610 },
6611 ast::LabeledArg {
6612 label: Some(ast::Identifier::new(LINE_END_PARAM)),
6613 arg: end_ast,
6614 },
6615 ],
6616 digest: None,
6617 non_code_meta: Default::default(),
6618 })))
6619}
6620
6621pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6623 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6624 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6625 unlabeled: None,
6626 arguments: vec![
6627 ast::LabeledArg {
6628 label: Some(ast::Identifier::new(ARC_START_PARAM)),
6629 arg: start_ast,
6630 },
6631 ast::LabeledArg {
6632 label: Some(ast::Identifier::new(ARC_END_PARAM)),
6633 arg: end_ast,
6634 },
6635 ast::LabeledArg {
6636 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6637 arg: center_ast,
6638 },
6639 ],
6640 digest: None,
6641 non_code_meta: Default::default(),
6642 })))
6643}
6644
6645pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6647 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6648 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6649 unlabeled: None,
6650 arguments: vec![
6651 ast::LabeledArg {
6652 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6653 arg: start_ast,
6654 },
6655 ast::LabeledArg {
6656 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6657 arg: center_ast,
6658 },
6659 ],
6660 digest: None,
6661 non_code_meta: Default::default(),
6662 })))
6663}
6664
6665pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6667 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6668 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6669 unlabeled: Some(line_expr),
6670 arguments: Default::default(),
6671 digest: None,
6672 non_code_meta: Default::default(),
6673 })))
6674}
6675
6676pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6678 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6679 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6680 unlabeled: Some(line_expr),
6681 arguments: Default::default(),
6682 digest: None,
6683 non_code_meta: Default::default(),
6684 })))
6685}
6686
6687pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6689 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6690 object: object_expr,
6691 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6692 name: ast::Node::no_src(ast::Identifier {
6693 name: property.to_string(),
6694 digest: None,
6695 }),
6696 path: Vec::new(),
6697 abs_path: false,
6698 digest: None,
6699 }))),
6700 computed: false,
6701 digest: None,
6702 })))
6703}
6704
6705pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
6706 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6707 object: object_expr,
6708 property: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
6709 value: index as f64,
6710 suffix: NumericSuffix::None,
6711 raw: index.to_string(),
6712 digest: None,
6713 })))),
6714 computed: true,
6715 digest: None,
6716 })))
6717}
6718
6719fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
6721 let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6723 position.x,
6724 )?))));
6725 let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6726 position.y,
6727 )?))));
6728 let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6729 elements: vec![x_literal, y_literal],
6730 digest: None,
6731 non_code_meta: Default::default(),
6732 })));
6733
6734 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6736 elements: vec![point_expr, point_array],
6737 digest: None,
6738 non_code_meta: Default::default(),
6739 })));
6740
6741 Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
6743 ast::CallExpressionKw {
6744 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
6745 unlabeled: Some(array_expr),
6746 arguments: Default::default(),
6747 digest: None,
6748 non_code_meta: Default::default(),
6749 },
6750 ))))
6751}
6752
6753pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
6755 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6756 elements: line_exprs,
6757 digest: None,
6758 non_code_meta: Default::default(),
6759 })));
6760
6761 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6763 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
6764 unlabeled: Some(array_expr),
6765 arguments: Default::default(),
6766 digest: None,
6767 non_code_meta: Default::default(),
6768 })))
6769}
6770
6771pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
6773 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6774 elements: segment_exprs,
6775 digest: None,
6776 non_code_meta: Default::default(),
6777 })));
6778
6779 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6780 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
6781 unlabeled: Some(array_expr),
6782 arguments: Default::default(),
6783 digest: None,
6784 non_code_meta: Default::default(),
6785 })))
6786}
6787
6788pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
6790 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6791 elements: vec![seg1_expr, seg2_expr],
6792 digest: None,
6793 non_code_meta: Default::default(),
6794 })));
6795
6796 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6797 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
6798 unlabeled: Some(array_expr),
6799 arguments: Default::default(),
6800 digest: None,
6801 non_code_meta: Default::default(),
6802 })))
6803}
6804
6805pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
6807 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6808 elements: input_exprs,
6809 digest: None,
6810 non_code_meta: Default::default(),
6811 })));
6812 let arguments = vec![ast::LabeledArg {
6813 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
6814 arg: axis_expr,
6815 }];
6816
6817 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6818 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
6819 unlabeled: Some(array_expr),
6820 arguments,
6821 digest: None,
6822 non_code_meta: Default::default(),
6823 })))
6824}
6825
6826pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
6828 let arguments = vec![ast::LabeledArg {
6829 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
6830 arg: point_expr,
6831 }];
6832
6833 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6834 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
6835 unlabeled: Some(segment_expr),
6836 arguments,
6837 digest: None,
6838 non_code_meta: Default::default(),
6839 })))
6840}
6841
6842#[cfg(test)]
6843mod tests {
6844 use super::*;
6845 use crate::engine::PlaneName;
6846 use crate::execution::cache::SketchModeState;
6847 use crate::execution::cache::clear_mem_cache;
6848 use crate::execution::cache::read_old_memory;
6849 use crate::execution::cache::write_old_memory;
6850 use crate::front::Distance;
6851 use crate::front::Fixed;
6852 use crate::front::FixedPoint;
6853 use crate::front::Midpoint;
6854 use crate::front::Object;
6855 use crate::front::Plane;
6856 use crate::front::Sketch;
6857 use crate::front::Tangent;
6858 use crate::frontend::sketch::Vertical;
6859 use crate::pretty::NumericSuffix;
6860
6861 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
6862 for object in &scene_graph.objects {
6863 if let ObjectKind::Sketch(_) = &object.kind {
6864 return Some(object);
6865 }
6866 }
6867 None
6868 }
6869
6870 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
6871 for object in &scene_graph.objects {
6872 if let ObjectKind::Face(_) = &object.kind {
6873 return Some(object);
6874 }
6875 }
6876 None
6877 }
6878
6879 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
6880 for object in &scene_graph.objects {
6881 if matches!(&object.kind, ObjectKind::Wall(_)) {
6882 return Some(object.id);
6883 }
6884 }
6885 None
6886 }
6887
6888 #[test]
6889 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
6890 let source = "\
6891region001 = region(point = [0.1, 0.1], sketch = s)
6892extrude001 = extrude(region001, length = 5)
6893revolve001 = revolve(region001, axis = Y)
6894sweep001 = sweep(region001, path = path001)
6895loft001 = loft(region001)
6896not_sweep001 = shell(extrude001, faces = [], thickness = 1)
6897";
6898
6899 let program = Program::parse(source).unwrap().0.unwrap();
6900
6901 assert_eq!(
6902 region_name_from_sweep_variable(&program.ast, "extrude001"),
6903 Some("region001".to_owned())
6904 );
6905 assert_eq!(
6906 region_name_from_sweep_variable(&program.ast, "revolve001"),
6907 Some("region001".to_owned())
6908 );
6909 assert_eq!(
6910 region_name_from_sweep_variable(&program.ast, "sweep001"),
6911 Some("region001".to_owned())
6912 );
6913 assert_eq!(
6914 region_name_from_sweep_variable(&program.ast, "loft001"),
6915 Some("region001".to_owned())
6916 );
6917 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
6918 }
6919
6920 #[track_caller]
6921 fn expect_sketch(object: &Object) -> &Sketch {
6922 if let ObjectKind::Sketch(sketch) = &object.kind {
6923 sketch
6924 } else {
6925 panic!("Object is not a sketch: {:?}", object);
6926 }
6927 }
6928
6929 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
6930 let point_object = scene_graph.objects.get(point_id.0).unwrap();
6931 let ObjectKind::Segment {
6932 segment: Segment::Point(point),
6933 } = &point_object.kind
6934 else {
6935 panic!("Object is not a point segment: {point_object:?}");
6936 };
6937 point.position.clone()
6938 }
6939
6940 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
6941 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
6942 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
6943 }
6944
6945 fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
6948 Point2d {
6949 x: Expr::Var(Number {
6950 value: x,
6951 units: NumericSuffix::Mm,
6952 }),
6953 y: Expr::Var(Number {
6954 value: y,
6955 units: NumericSuffix::Mm,
6956 }),
6957 }
6958 }
6959
6960 fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
6963 Point2d {
6964 x: Number {
6965 value: x,
6966 units: NumericSuffix::Mm,
6967 },
6968 y: Number {
6969 value: y,
6970 units: NumericSuffix::Mm,
6971 },
6972 }
6973 }
6974
6975 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
6976 LineCtor {
6977 start: Point2d {
6978 x: Expr::Number(Number { value: start_x, units }),
6979 y: Expr::Number(Number { value: start_y, units }),
6980 },
6981 end: Point2d {
6982 x: Expr::Number(Number { value: end_x, units }),
6983 y: Expr::Number(Number { value: end_y, units }),
6984 },
6985 construction: None,
6986 }
6987 }
6988
6989 async fn create_sketch_with_single_line(
6990 frontend: &mut FrontendState,
6991 ctx: &ExecutorContext,
6992 mock_ctx: &ExecutorContext,
6993 version: Version,
6994 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
6995 frontend.program = Program::empty();
6996
6997 let sketch_args = SketchCtor {
6998 on: Plane::Default(PlaneName::Xy),
6999 };
7000 let (_src_delta, _scene_delta, sketch_id) = frontend
7001 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7002 .await
7003 .unwrap();
7004
7005 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7006 let (source_delta, scene_graph_delta) = frontend
7007 .add_segment(mock_ctx, version, sketch_id, segment, None)
7008 .await
7009 .unwrap();
7010 let line_id = *scene_graph_delta
7011 .new_objects
7012 .last()
7013 .expect("Expected line object id to be created");
7014
7015 (sketch_id, line_id, source_delta, scene_graph_delta)
7016 }
7017
7018 async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7019 frontend.program = program.clone();
7020 let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7021 frontend.update_state_after_exec(outcome, true);
7022 }
7023
7024 #[tokio::test(flavor = "multi_thread")]
7025 async fn test_sketch_checkpoint_round_trip_restores_state() {
7026 let mut frontend = FrontendState::new();
7027 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7028 let mock_ctx = ExecutorContext::new_mock(None).await;
7029 let version = Version(0);
7030
7031 let (sketch_id, line_id, source_delta, scene_graph_delta) =
7032 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7033
7034 let expected_source = source_delta.text.clone();
7035 let expected_scene_graph = frontend.scene_graph.clone();
7036 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7037 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7038
7039 let checkpoint_id = frontend
7040 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7041 .await
7042 .unwrap();
7043
7044 let edited_segments = vec![ExistingSegmentCtor {
7045 id: line_id,
7046 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7047 }];
7048 let (edited_source, _edited_scene) = frontend
7049 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7050 .await
7051 .unwrap();
7052 assert_ne!(edited_source.text, expected_source);
7053
7054 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7055
7056 assert_eq!(restored.source_delta.text, expected_source);
7057 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7058 assert!(restored.scene_graph_delta.invalidates_ids);
7059 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7060 assert_eq!(frontend.scene_graph, expected_scene_graph);
7061 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7062
7063 ctx.close().await;
7064 }
7065
7066 #[tokio::test(flavor = "multi_thread")]
7067 async fn test_sketch_checkpoints_prune_oldest_entries() {
7068 let mut frontend = FrontendState::new();
7069 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7070 let mock_ctx = ExecutorContext::new_mock(None).await;
7071 let version = Version(0);
7072
7073 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7074 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7075
7076 let mut checkpoint_ids = Vec::new();
7077 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7078 checkpoint_ids.push(
7079 frontend
7080 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7081 .await
7082 .unwrap(),
7083 );
7084 }
7085
7086 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7087 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7088
7089 let oldest_retained = checkpoint_ids[3];
7090 assert_eq!(
7091 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7092 Some(oldest_retained)
7093 );
7094
7095 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7096 assert!(evicted_restore.is_err());
7097 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7098
7099 frontend
7100 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7101 .await
7102 .unwrap();
7103
7104 ctx.close().await;
7105 }
7106
7107 #[tokio::test(flavor = "multi_thread")]
7108 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7109 let mut frontend = FrontendState::new();
7110 let missing_checkpoint = SketchCheckpointId::new(999);
7111
7112 let err = frontend
7113 .restore_sketch_checkpoint(missing_checkpoint)
7114 .await
7115 .expect_err("Expected restore to fail for missing checkpoint");
7116
7117 assert!(err.msg.contains("Sketch checkpoint not found"));
7118 }
7119
7120 #[tokio::test(flavor = "multi_thread")]
7121 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7122 let mut frontend = FrontendState::new();
7123 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7124 let mock_ctx = ExecutorContext::new_mock(None).await;
7125 let version = Version(0);
7126
7127 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7128 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7129
7130 let checkpoint_a = frontend
7131 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7132 .await
7133 .unwrap();
7134 let checkpoint_b = frontend
7135 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7136 .await
7137 .unwrap();
7138 assert_eq!(frontend.sketch_checkpoints.len(), 2);
7139
7140 frontend.clear_sketch_checkpoints();
7141 assert!(frontend.sketch_checkpoints.is_empty());
7142 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7143 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7144
7145 ctx.close().await;
7146 }
7147
7148 #[tokio::test(flavor = "multi_thread")]
7149 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7150 let mut frontend = FrontendState::new();
7151 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7152 let mock_ctx = ExecutorContext::new_mock(None).await;
7153 let version = Version(0);
7154
7155 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7156 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7157 let old_source = source_delta.text.clone();
7158 let old_checkpoint = frontend
7159 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7160 .await
7161 .unwrap();
7162 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7163
7164 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
7165 .unwrap()
7166 .0
7167 .unwrap();
7168
7169 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7170 let SetProgramOutcome::Success {
7171 checkpoint_id: Some(new_checkpoint),
7172 ..
7173 } = result
7174 else {
7175 panic!("Expected Success with a fresh checkpoint baseline");
7176 };
7177
7178 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7179
7180 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7181 assert_eq!(old_restore.source_delta.text, old_source);
7182
7183 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7184 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7185
7186 ctx.close().await;
7187 }
7188
7189 #[tokio::test(flavor = "multi_thread")]
7190 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7191 let mut frontend = FrontendState::new();
7192 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7193 let mock_ctx = ExecutorContext::new_mock(None).await;
7194 let version = Version(0);
7195
7196 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7197 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7198 let old_checkpoint = frontend
7199 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7200 .await
7201 .unwrap();
7202 let checkpoint_count_before = frontend.sketch_checkpoints.len();
7203
7204 let failing_program = Program::parse(
7205 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7206 )
7207 .unwrap()
7208 .0
7209 .unwrap();
7210
7211 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7212 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7213 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7214 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7215
7216 ctx.close().await;
7217 }
7218
7219 #[tokio::test(flavor = "multi_thread")]
7220 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7221 let mut frontend = FrontendState::new();
7222 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7223
7224 let program = Program::parse(
7225 "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",
7226 )
7227 .unwrap()
7228 .0
7229 .unwrap();
7230 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7231 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7232 panic!("Expected successful baseline program execution");
7233 };
7234
7235 clear_mem_cache().await;
7236 assert!(read_old_memory().await.is_none());
7237
7238 let checkpoint_without_mock_memory = frontend
7239 .create_sketch_checkpoint((*exec_outcome).clone())
7240 .await
7241 .unwrap();
7242
7243 write_old_memory(SketchModeState::new_for_tests()).await;
7244 assert!(read_old_memory().await.is_some());
7245
7246 let checkpoint_with_mock_memory = frontend
7247 .create_sketch_checkpoint((*exec_outcome).clone())
7248 .await
7249 .unwrap();
7250
7251 clear_mem_cache().await;
7252 assert!(read_old_memory().await.is_none());
7253
7254 frontend
7255 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7256 .await
7257 .unwrap();
7258 assert!(read_old_memory().await.is_some());
7259
7260 frontend
7261 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7262 .await
7263 .unwrap();
7264 assert!(read_old_memory().await.is_none());
7265
7266 ctx.close().await;
7267 }
7268
7269 #[tokio::test(flavor = "multi_thread")]
7270 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7271 let source = "\
7272sketch(on = XY) {
7273 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7274}
7275
7276bad = missing_name
7277";
7278 let program = Program::parse(source).unwrap().0.unwrap();
7279
7280 let mut frontend = FrontendState::new();
7281
7282 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7283 let mock_ctx = ExecutorContext::new_mock(None).await;
7284 let version = Version(0);
7285 let project_id = ProjectId(0);
7286 let file_id = FileId(0);
7287
7288 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7289 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7290 };
7291
7292 let sketch_id = frontend
7293 .scene_graph
7294 .objects
7295 .iter()
7296 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7297 .expect("Expected sketch object from errored hack_set_program");
7298
7299 frontend
7300 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7301 .await
7302 .unwrap();
7303
7304 ctx.close().await;
7305 mock_ctx.close().await;
7306 }
7307
7308 #[tokio::test(flavor = "multi_thread")]
7309 async fn test_new_sketch_add_point_edit_point() {
7310 let program = Program::empty();
7311
7312 let mut frontend = FrontendState::new();
7313 frontend.program = program;
7314
7315 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7316 let mock_ctx = ExecutorContext::new_mock(None).await;
7317 let version = Version(0);
7318
7319 let sketch_args = SketchCtor {
7320 on: Plane::Default(PlaneName::Xy),
7321 };
7322 let (_src_delta, scene_delta, sketch_id) = frontend
7323 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7324 .await
7325 .unwrap();
7326 assert_eq!(sketch_id, ObjectId(1));
7327 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7328 let sketch_object = &scene_delta.new_graph.objects[1];
7329 assert_eq!(sketch_object.id, ObjectId(1));
7330 assert_eq!(
7331 sketch_object.kind,
7332 ObjectKind::Sketch(Sketch {
7333 args: SketchCtor {
7334 on: Plane::Default(PlaneName::Xy)
7335 },
7336 plane: ObjectId(0),
7337 segments: vec![],
7338 constraints: vec![],
7339 })
7340 );
7341 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7342
7343 let point_ctor = PointCtor {
7344 position: Point2d {
7345 x: Expr::Number(Number {
7346 value: 1.0,
7347 units: NumericSuffix::Inch,
7348 }),
7349 y: Expr::Number(Number {
7350 value: 2.0,
7351 units: NumericSuffix::Inch,
7352 }),
7353 },
7354 };
7355 let segment = SegmentCtor::Point(point_ctor);
7356 let (src_delta, scene_delta) = frontend
7357 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7358 .await
7359 .unwrap();
7360 assert_eq!(
7361 src_delta.text.as_str(),
7362 "sketch001 = sketch(on = XY) {
7363 point(at = [1in, 2in])
7364}
7365"
7366 );
7367 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7368 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7369 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7370 assert_eq!(scene_object.id.0, i);
7371 }
7372
7373 let point_id = *scene_delta.new_objects.last().unwrap();
7374
7375 let point_ctor = PointCtor {
7376 position: Point2d {
7377 x: Expr::Number(Number {
7378 value: 3.0,
7379 units: NumericSuffix::Inch,
7380 }),
7381 y: Expr::Number(Number {
7382 value: 4.0,
7383 units: NumericSuffix::Inch,
7384 }),
7385 },
7386 };
7387 let segments = vec![ExistingSegmentCtor {
7388 id: point_id,
7389 ctor: SegmentCtor::Point(point_ctor),
7390 }];
7391 let (src_delta, scene_delta) = frontend
7392 .edit_segments(&mock_ctx, version, sketch_id, segments)
7393 .await
7394 .unwrap();
7395 assert_eq!(
7396 src_delta.text.as_str(),
7397 "sketch001 = sketch(on = XY) {
7398 point(at = [3in, 4in])
7399}
7400"
7401 );
7402 assert_eq!(scene_delta.new_objects, vec![]);
7403 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7404
7405 ctx.close().await;
7406 mock_ctx.close().await;
7407 }
7408
7409 #[tokio::test(flavor = "multi_thread")]
7410 async fn test_new_sketch_add_line_edit_line() {
7411 let program = Program::empty();
7412
7413 let mut frontend = FrontendState::new();
7414 frontend.program = program;
7415
7416 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7417 let mock_ctx = ExecutorContext::new_mock(None).await;
7418 let version = Version(0);
7419
7420 let sketch_args = SketchCtor {
7421 on: Plane::Default(PlaneName::Xy),
7422 };
7423 let (_src_delta, scene_delta, sketch_id) = frontend
7424 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7425 .await
7426 .unwrap();
7427 assert_eq!(sketch_id, ObjectId(1));
7428 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7429 let sketch_object = &scene_delta.new_graph.objects[1];
7430 assert_eq!(sketch_object.id, ObjectId(1));
7431 assert_eq!(
7432 sketch_object.kind,
7433 ObjectKind::Sketch(Sketch {
7434 args: SketchCtor {
7435 on: Plane::Default(PlaneName::Xy)
7436 },
7437 plane: ObjectId(0),
7438 segments: vec![],
7439 constraints: vec![],
7440 })
7441 );
7442 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7443
7444 let line_ctor = LineCtor {
7445 start: Point2d {
7446 x: Expr::Number(Number {
7447 value: 0.0,
7448 units: NumericSuffix::Mm,
7449 }),
7450 y: Expr::Number(Number {
7451 value: 0.0,
7452 units: NumericSuffix::Mm,
7453 }),
7454 },
7455 end: Point2d {
7456 x: Expr::Number(Number {
7457 value: 10.0,
7458 units: NumericSuffix::Mm,
7459 }),
7460 y: Expr::Number(Number {
7461 value: 10.0,
7462 units: NumericSuffix::Mm,
7463 }),
7464 },
7465 construction: None,
7466 };
7467 let segment = SegmentCtor::Line(line_ctor);
7468 let (src_delta, scene_delta) = frontend
7469 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7470 .await
7471 .unwrap();
7472 assert_eq!(
7473 src_delta.text.as_str(),
7474 "sketch001 = sketch(on = XY) {
7475 line(start = [0mm, 0mm], end = [10mm, 10mm])
7476}
7477"
7478 );
7479 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7480 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7481 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7482 assert_eq!(scene_object.id.0, i);
7483 }
7484
7485 let line = *scene_delta.new_objects.last().unwrap();
7487
7488 let line_ctor = LineCtor {
7489 start: Point2d {
7490 x: Expr::Number(Number {
7491 value: 1.0,
7492 units: NumericSuffix::Mm,
7493 }),
7494 y: Expr::Number(Number {
7495 value: 2.0,
7496 units: NumericSuffix::Mm,
7497 }),
7498 },
7499 end: Point2d {
7500 x: Expr::Number(Number {
7501 value: 13.0,
7502 units: NumericSuffix::Mm,
7503 }),
7504 y: Expr::Number(Number {
7505 value: 14.0,
7506 units: NumericSuffix::Mm,
7507 }),
7508 },
7509 construction: None,
7510 };
7511 let segments = vec![ExistingSegmentCtor {
7512 id: line,
7513 ctor: SegmentCtor::Line(line_ctor),
7514 }];
7515 let (src_delta, scene_delta) = frontend
7516 .edit_segments(&mock_ctx, version, sketch_id, segments)
7517 .await
7518 .unwrap();
7519 assert_eq!(
7520 src_delta.text.as_str(),
7521 "sketch001 = sketch(on = XY) {
7522 line(start = [1mm, 2mm], end = [13mm, 14mm])
7523}
7524"
7525 );
7526 assert_eq!(scene_delta.new_objects, vec![]);
7527 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7528
7529 ctx.close().await;
7530 mock_ctx.close().await;
7531 }
7532
7533 #[tokio::test(flavor = "multi_thread")]
7534 async fn test_new_sketch_add_arc_edit_arc() {
7535 let program = Program::empty();
7536
7537 let mut frontend = FrontendState::new();
7538 frontend.program = program;
7539
7540 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7541 let mock_ctx = ExecutorContext::new_mock(None).await;
7542 let version = Version(0);
7543
7544 let sketch_args = SketchCtor {
7545 on: Plane::Default(PlaneName::Xy),
7546 };
7547 let (_src_delta, scene_delta, sketch_id) = frontend
7548 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7549 .await
7550 .unwrap();
7551 assert_eq!(sketch_id, ObjectId(1));
7552 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7553 let sketch_object = &scene_delta.new_graph.objects[1];
7554 assert_eq!(sketch_object.id, ObjectId(1));
7555 assert_eq!(
7556 sketch_object.kind,
7557 ObjectKind::Sketch(Sketch {
7558 args: SketchCtor {
7559 on: Plane::Default(PlaneName::Xy),
7560 },
7561 plane: ObjectId(0),
7562 segments: vec![],
7563 constraints: vec![],
7564 })
7565 );
7566 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7567
7568 let arc_ctor = ArcCtor {
7569 start: Point2d {
7570 x: Expr::Var(Number {
7571 value: 0.0,
7572 units: NumericSuffix::Mm,
7573 }),
7574 y: Expr::Var(Number {
7575 value: 0.0,
7576 units: NumericSuffix::Mm,
7577 }),
7578 },
7579 end: Point2d {
7580 x: Expr::Var(Number {
7581 value: 10.0,
7582 units: NumericSuffix::Mm,
7583 }),
7584 y: Expr::Var(Number {
7585 value: 10.0,
7586 units: NumericSuffix::Mm,
7587 }),
7588 },
7589 center: Point2d {
7590 x: Expr::Var(Number {
7591 value: 10.0,
7592 units: NumericSuffix::Mm,
7593 }),
7594 y: Expr::Var(Number {
7595 value: 0.0,
7596 units: NumericSuffix::Mm,
7597 }),
7598 },
7599 construction: None,
7600 };
7601 let segment = SegmentCtor::Arc(arc_ctor);
7602 let (src_delta, scene_delta) = frontend
7603 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7604 .await
7605 .unwrap();
7606 assert_eq!(
7607 src_delta.text.as_str(),
7608 "sketch001 = sketch(on = XY) {
7609 arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
7610}
7611"
7612 );
7613 assert_eq!(
7614 scene_delta.new_objects,
7615 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
7616 );
7617 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7618 assert_eq!(scene_object.id.0, i);
7619 }
7620 assert_eq!(scene_delta.new_graph.objects.len(), 6);
7621
7622 let arc = *scene_delta.new_objects.last().unwrap();
7624
7625 let arc_ctor = ArcCtor {
7626 start: Point2d {
7627 x: Expr::Var(Number {
7628 value: 1.0,
7629 units: NumericSuffix::Mm,
7630 }),
7631 y: Expr::Var(Number {
7632 value: 2.0,
7633 units: NumericSuffix::Mm,
7634 }),
7635 },
7636 end: Point2d {
7637 x: Expr::Var(Number {
7638 value: 13.0,
7639 units: NumericSuffix::Mm,
7640 }),
7641 y: Expr::Var(Number {
7642 value: 14.0,
7643 units: NumericSuffix::Mm,
7644 }),
7645 },
7646 center: Point2d {
7647 x: Expr::Var(Number {
7648 value: 13.0,
7649 units: NumericSuffix::Mm,
7650 }),
7651 y: Expr::Var(Number {
7652 value: 2.0,
7653 units: NumericSuffix::Mm,
7654 }),
7655 },
7656 construction: None,
7657 };
7658 let segments = vec![ExistingSegmentCtor {
7659 id: arc,
7660 ctor: SegmentCtor::Arc(arc_ctor),
7661 }];
7662 let (src_delta, scene_delta) = frontend
7663 .edit_segments(&mock_ctx, version, sketch_id, segments)
7664 .await
7665 .unwrap();
7666 assert_eq!(
7667 src_delta.text.as_str(),
7668 "sketch001 = sketch(on = XY) {
7669 arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
7670}
7671"
7672 );
7673 assert_eq!(scene_delta.new_objects, vec![]);
7674 assert_eq!(scene_delta.new_graph.objects.len(), 6);
7675
7676 ctx.close().await;
7677 mock_ctx.close().await;
7678 }
7679
7680 #[tokio::test(flavor = "multi_thread")]
7681 async fn test_new_sketch_add_circle_edit_circle() {
7682 let program = Program::empty();
7683
7684 let mut frontend = FrontendState::new();
7685 frontend.program = program;
7686
7687 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7688 let mock_ctx = ExecutorContext::new_mock(None).await;
7689 let version = Version(0);
7690
7691 let sketch_args = SketchCtor {
7692 on: Plane::Default(PlaneName::Xy),
7693 };
7694 let (_src_delta, _scene_delta, sketch_id) = frontend
7695 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7696 .await
7697 .unwrap();
7698
7699 let circle_ctor = CircleCtor {
7701 start: Point2d {
7702 x: Expr::Var(Number {
7703 value: 5.0,
7704 units: NumericSuffix::Mm,
7705 }),
7706 y: Expr::Var(Number {
7707 value: 0.0,
7708 units: NumericSuffix::Mm,
7709 }),
7710 },
7711 center: Point2d {
7712 x: Expr::Var(Number {
7713 value: 0.0,
7714 units: NumericSuffix::Mm,
7715 }),
7716 y: Expr::Var(Number {
7717 value: 0.0,
7718 units: NumericSuffix::Mm,
7719 }),
7720 },
7721 construction: None,
7722 };
7723 let segment = SegmentCtor::Circle(circle_ctor);
7724 let (src_delta, scene_delta) = frontend
7725 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7726 .await
7727 .unwrap();
7728 assert_eq!(
7729 src_delta.text.as_str(),
7730 "sketch001 = sketch(on = XY) {
7731 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7732}
7733"
7734 );
7735 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7737 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7738
7739 let circle = *scene_delta.new_objects.last().unwrap();
7740
7741 let circle_ctor = CircleCtor {
7743 start: Point2d {
7744 x: Expr::Var(Number {
7745 value: 10.0,
7746 units: NumericSuffix::Mm,
7747 }),
7748 y: Expr::Var(Number {
7749 value: 0.0,
7750 units: NumericSuffix::Mm,
7751 }),
7752 },
7753 center: Point2d {
7754 x: Expr::Var(Number {
7755 value: 3.0,
7756 units: NumericSuffix::Mm,
7757 }),
7758 y: Expr::Var(Number {
7759 value: 4.0,
7760 units: NumericSuffix::Mm,
7761 }),
7762 },
7763 construction: None,
7764 };
7765 let segments = vec![ExistingSegmentCtor {
7766 id: circle,
7767 ctor: SegmentCtor::Circle(circle_ctor),
7768 }];
7769 let (src_delta, scene_delta) = frontend
7770 .edit_segments(&mock_ctx, version, sketch_id, segments)
7771 .await
7772 .unwrap();
7773 assert_eq!(
7774 src_delta.text.as_str(),
7775 "sketch001 = sketch(on = XY) {
7776 circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
7777}
7778"
7779 );
7780 assert_eq!(scene_delta.new_objects, vec![]);
7781 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7782
7783 ctx.close().await;
7784 mock_ctx.close().await;
7785 }
7786
7787 #[tokio::test(flavor = "multi_thread")]
7788 async fn test_delete_circle() {
7789 let initial_source = "sketch001 = sketch(on = XY) {
7790 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7791}
7792";
7793
7794 let program = Program::parse(initial_source).unwrap().0.unwrap();
7795 let mut frontend = FrontendState::new();
7796
7797 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7798 let mock_ctx = ExecutorContext::new_mock(None).await;
7799 let version = Version(0);
7800
7801 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7802 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7803 let sketch_id = sketch_object.id;
7804 let sketch = expect_sketch(sketch_object);
7805
7806 assert_eq!(sketch.segments.len(), 3);
7808 let circle_id = sketch.segments[2];
7809
7810 let (src_delta, scene_delta) = frontend
7812 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
7813 .await
7814 .unwrap();
7815 assert_eq!(
7816 src_delta.text.as_str(),
7817 "sketch001 = sketch(on = XY) {
7818}
7819"
7820 );
7821 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7822 let new_sketch = expect_sketch(new_sketch_object);
7823 assert_eq!(new_sketch.segments.len(), 0);
7824
7825 ctx.close().await;
7826 mock_ctx.close().await;
7827 }
7828
7829 #[tokio::test(flavor = "multi_thread")]
7830 async fn test_edit_circle_via_point() {
7831 let initial_source = "sketch001 = sketch(on = XY) {
7832 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7833}
7834";
7835
7836 let program = Program::parse(initial_source).unwrap().0.unwrap();
7837 let mut frontend = FrontendState::new();
7838
7839 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7840 let mock_ctx = ExecutorContext::new_mock(None).await;
7841 let version = Version(0);
7842
7843 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7844 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7845 let sketch_id = sketch_object.id;
7846 let sketch = expect_sketch(sketch_object);
7847
7848 let circle_id = sketch
7850 .segments
7851 .iter()
7852 .copied()
7853 .find(|seg_id| {
7854 matches!(
7855 &frontend.scene_graph.objects[seg_id.0].kind,
7856 ObjectKind::Segment {
7857 segment: Segment::Circle(_)
7858 }
7859 )
7860 })
7861 .expect("Expected a circle segment in sketch");
7862 let circle_object = &frontend.scene_graph.objects[circle_id.0];
7863 let ObjectKind::Segment {
7864 segment: Segment::Circle(circle),
7865 } = &circle_object.kind
7866 else {
7867 panic!("Expected circle segment, got: {:?}", circle_object.kind);
7868 };
7869 let start_point_id = circle.start;
7870
7871 let segments = vec![ExistingSegmentCtor {
7873 id: start_point_id,
7874 ctor: SegmentCtor::Point(PointCtor {
7875 position: Point2d {
7876 x: Expr::Var(Number {
7877 value: 7.0,
7878 units: NumericSuffix::Mm,
7879 }),
7880 y: Expr::Var(Number {
7881 value: 1.0,
7882 units: NumericSuffix::Mm,
7883 }),
7884 },
7885 }),
7886 }];
7887 let (src_delta, _scene_delta) = frontend
7888 .edit_segments(&mock_ctx, version, sketch_id, segments)
7889 .await
7890 .unwrap();
7891 assert_eq!(
7892 src_delta.text.as_str(),
7893 "sketch001 = sketch(on = XY) {
7894 circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
7895}
7896"
7897 );
7898
7899 ctx.close().await;
7900 mock_ctx.close().await;
7901 }
7902
7903 #[tokio::test(flavor = "multi_thread")]
7904 async fn test_add_line_when_sketch_block_uses_variable() {
7905 let initial_source = "s = sketch(on = XY) {}
7906";
7907
7908 let program = Program::parse(initial_source).unwrap().0.unwrap();
7909
7910 let mut frontend = FrontendState::new();
7911
7912 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7913 let mock_ctx = ExecutorContext::new_mock(None).await;
7914 let version = Version(0);
7915
7916 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7917 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7918 let sketch_id = sketch_object.id;
7919
7920 let line_ctor = LineCtor {
7921 start: Point2d {
7922 x: Expr::Number(Number {
7923 value: 0.0,
7924 units: NumericSuffix::Mm,
7925 }),
7926 y: Expr::Number(Number {
7927 value: 0.0,
7928 units: NumericSuffix::Mm,
7929 }),
7930 },
7931 end: Point2d {
7932 x: Expr::Number(Number {
7933 value: 10.0,
7934 units: NumericSuffix::Mm,
7935 }),
7936 y: Expr::Number(Number {
7937 value: 10.0,
7938 units: NumericSuffix::Mm,
7939 }),
7940 },
7941 construction: None,
7942 };
7943 let segment = SegmentCtor::Line(line_ctor);
7944 let (src_delta, scene_delta) = frontend
7945 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7946 .await
7947 .unwrap();
7948 assert_eq!(
7949 src_delta.text.as_str(),
7950 "s = sketch(on = XY) {
7951 line(start = [0mm, 0mm], end = [10mm, 10mm])
7952}
7953"
7954 );
7955 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7956 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7957
7958 ctx.close().await;
7959 mock_ctx.close().await;
7960 }
7961
7962 #[tokio::test(flavor = "multi_thread")]
7963 async fn test_new_sketch_add_line_delete_sketch() {
7964 let program = Program::empty();
7965
7966 let mut frontend = FrontendState::new();
7967 frontend.program = program;
7968
7969 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7970 let mock_ctx = ExecutorContext::new_mock(None).await;
7971 let version = Version(0);
7972
7973 let sketch_args = SketchCtor {
7974 on: Plane::Default(PlaneName::Xy),
7975 };
7976 let (_src_delta, scene_delta, sketch_id) = frontend
7977 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7978 .await
7979 .unwrap();
7980 assert_eq!(sketch_id, ObjectId(1));
7981 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7982 let sketch_object = &scene_delta.new_graph.objects[1];
7983 assert_eq!(sketch_object.id, ObjectId(1));
7984 assert_eq!(
7985 sketch_object.kind,
7986 ObjectKind::Sketch(Sketch {
7987 args: SketchCtor {
7988 on: Plane::Default(PlaneName::Xy)
7989 },
7990 plane: ObjectId(0),
7991 segments: vec![],
7992 constraints: vec![],
7993 })
7994 );
7995 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7996
7997 let line_ctor = LineCtor {
7998 start: Point2d {
7999 x: Expr::Number(Number {
8000 value: 0.0,
8001 units: NumericSuffix::Mm,
8002 }),
8003 y: Expr::Number(Number {
8004 value: 0.0,
8005 units: NumericSuffix::Mm,
8006 }),
8007 },
8008 end: Point2d {
8009 x: Expr::Number(Number {
8010 value: 10.0,
8011 units: NumericSuffix::Mm,
8012 }),
8013 y: Expr::Number(Number {
8014 value: 10.0,
8015 units: NumericSuffix::Mm,
8016 }),
8017 },
8018 construction: None,
8019 };
8020 let segment = SegmentCtor::Line(line_ctor);
8021 let (src_delta, scene_delta) = frontend
8022 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8023 .await
8024 .unwrap();
8025 assert_eq!(
8026 src_delta.text.as_str(),
8027 "sketch001 = sketch(on = XY) {
8028 line(start = [0mm, 0mm], end = [10mm, 10mm])
8029}
8030"
8031 );
8032 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8033
8034 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8035 assert_eq!(src_delta.text.as_str(), "");
8036 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8037
8038 ctx.close().await;
8039 mock_ctx.close().await;
8040 }
8041
8042 #[tokio::test(flavor = "multi_thread")]
8043 async fn test_delete_sketch_when_sketch_block_uses_variable() {
8044 let initial_source = "s = sketch(on = XY) {}
8045";
8046
8047 let program = Program::parse(initial_source).unwrap().0.unwrap();
8048
8049 let mut frontend = FrontendState::new();
8050
8051 let ctx = ExecutorContext::new_with_engine(
8052 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
8053 Default::default(),
8054 );
8055 let version = Version(0);
8056
8057 frontend.hack_set_program(&ctx, program).await.unwrap();
8058 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8059 let sketch_id = sketch_object.id;
8060
8061 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8062 assert_eq!(src_delta.text.as_str(), "");
8063 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8064
8065 ctx.close().await;
8066 }
8067
8068 #[tokio::test(flavor = "multi_thread")]
8069 async fn test_delete_sketch_after_comment() {
8070 let initial_source = "sketch001 = sketch(on = XZ) {
8071}
8072";
8073
8074 let program = Program::parse(initial_source).unwrap().0.unwrap();
8075 let mut frontend = FrontendState::new();
8076
8077 let ctx = ExecutorContext::new_with_engine(
8078 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
8079 Default::default(),
8080 );
8081 let version = Version(0);
8082
8083 frontend.hack_set_program(&ctx, program).await.unwrap();
8084 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8085 let sketch_id = sketch_object.id;
8086 let original_source = sketch_object.source.clone();
8087
8088 let commented_source = "// test 1
8089sketch001 = sketch(on = XZ) {
8090}
8091";
8092 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8093 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8094
8095 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8096 assert_eq!(cached_sketch_object.source, original_source);
8097
8098 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8099 assert!(
8100 !src_delta.text.contains("sketch001"),
8101 "sketch was not deleted: {}",
8102 src_delta.text
8103 );
8104 assert_eq!(src_delta.text.as_str(), "// test 1\n");
8106 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8107
8108 ctx.close().await;
8109 }
8110
8111 #[tokio::test(flavor = "multi_thread")]
8112 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8113 let initial_source = "sketch001 = sketch(on = XZ) {
8114}
8115foo = 1
8116";
8117
8118 let program = Program::parse(initial_source).unwrap().0.unwrap();
8119 let mut frontend = FrontendState::new();
8120
8121 let ctx = ExecutorContext::new_with_engine(
8122 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
8123 Default::default(),
8124 );
8125 let version = Version(0);
8126
8127 frontend.hack_set_program(&ctx, program).await.unwrap();
8128 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8129 let sketch_id = sketch_object.id;
8130
8131 let commented_source = "// keep me
8132sketch001 = sketch(on = XZ) {
8133}
8134foo = 1
8135";
8136 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8137 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8138
8139 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8140 assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
8142
8143 ctx.close().await;
8144 }
8145
8146 #[tokio::test(flavor = "multi_thread")]
8147 async fn test_delete_segment_preserves_pre_comment() {
8148 let initial_source = "\
8149sketch(on = XY) {
8150 point(at = [var 1, var 2])
8151 // describe the middle point
8152 point(at = [var 3, var 4])
8153 point(at = [var 5, var 6])
8154}
8155";
8156
8157 let program = Program::parse(initial_source).unwrap().0.unwrap();
8158 let mut frontend = FrontendState::new();
8159
8160 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8161 let mock_ctx = ExecutorContext::new_mock(None).await;
8162 let version = Version(0);
8163
8164 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8165 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8166 let sketch_id = sketch_object.id;
8167 let sketch = expect_sketch(sketch_object);
8168
8169 let middle_point_id = *sketch.segments.get(1).unwrap();
8170
8171 let (src_delta, _scene_delta) = frontend
8172 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8173 .await
8174 .unwrap();
8175 assert_eq!(
8178 src_delta.text.as_str(),
8179 "\
8180sketch(on = XY) {
8181 point(at = [var 1, var 2])
8182 // describe the middle point
8183 point(at = [var 5, var 6])
8184}
8185"
8186 );
8187
8188 ctx.close().await;
8189 mock_ctx.close().await;
8190 }
8191
8192 #[tokio::test(flavor = "multi_thread")]
8193 async fn test_delete_last_segment_preserves_pre_comment() {
8194 let initial_source = "\
8195sketch(on = XY) {
8196 point(at = [var 1, var 2])
8197 // describe the trailing point
8198 point(at = [var 3, var 4])
8199}
8200";
8201
8202 let program = Program::parse(initial_source).unwrap().0.unwrap();
8203 let mut frontend = FrontendState::new();
8204
8205 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8206 let mock_ctx = ExecutorContext::new_mock(None).await;
8207 let version = Version(0);
8208
8209 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8210 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8211 let sketch_id = sketch_object.id;
8212 let sketch = expect_sketch(sketch_object);
8213
8214 let last_point_id = *sketch.segments.last().unwrap();
8215
8216 let (src_delta, _scene_delta) = frontend
8217 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8218 .await
8219 .unwrap();
8220 assert_eq!(
8223 src_delta.text.as_str(),
8224 "\
8225sketch(on = XY) {
8226 point(at = [var 1, var 2])
8227 // describe the trailing point
8228}
8229"
8230 );
8231
8232 ctx.close().await;
8233 mock_ctx.close().await;
8234 }
8235
8236 #[tokio::test(flavor = "multi_thread")]
8237 async fn test_delete_segment_drops_inline_trailing_comment() {
8238 let initial_source = "\
8239sketch(on = XY) {
8240 point(at = [var 1, var 2])
8241 point(at = [var 3, var 4]) // same-line note that gets dropped
8242 point(at = [var 5, var 6])
8243}
8244";
8245
8246 let program = Program::parse(initial_source).unwrap().0.unwrap();
8247 let mut frontend = FrontendState::new();
8248
8249 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8250 let mock_ctx = ExecutorContext::new_mock(None).await;
8251 let version = Version(0);
8252
8253 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8254 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8255 let sketch_id = sketch_object.id;
8256 let sketch = expect_sketch(sketch_object);
8257
8258 let middle_point_id = *sketch.segments.get(1).unwrap();
8259
8260 let (src_delta, _scene_delta) = frontend
8261 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8262 .await
8263 .unwrap();
8264 assert!(
8266 !src_delta.text.contains("same-line note"),
8267 "inline comment should have been removed: {}",
8268 src_delta.text
8269 );
8270
8271 ctx.close().await;
8272 mock_ctx.close().await;
8273 }
8274
8275 #[tokio::test(flavor = "multi_thread")]
8276 async fn test_delete_segments_preserves_block_comments_across_positions() {
8277 let initial_source = "\
8285sketch(on = XY) {
8286 /* above first - moves to middle */
8287 point(at = [var 1, var 2]) /* same-line on first - dropped */
8288 /* above middle - stays */
8289 point(at = [var 3, var 4])
8290 /* above last - moves to trailing meta */
8291 point(at = [var 5, var 6])
8292}
8293";
8294
8295 let program = Program::parse(initial_source).unwrap().0.unwrap();
8296 let mut frontend = FrontendState::new();
8297
8298 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8299 let mock_ctx = ExecutorContext::new_mock(None).await;
8300 let version = Version(0);
8301
8302 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8303 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8304 let sketch_id = sketch_object.id;
8305 let sketch = expect_sketch(sketch_object);
8306
8307 let first_point_id = *sketch.segments.first().unwrap();
8308 let last_point_id = *sketch.segments.last().unwrap();
8309
8310 let (src_delta, _scene_delta) = frontend
8311 .delete_objects(
8312 &mock_ctx,
8313 version,
8314 sketch_id,
8315 Vec::new(),
8316 vec![first_point_id, last_point_id],
8317 )
8318 .await
8319 .unwrap();
8320 assert_eq!(
8321 src_delta.text.as_str(),
8322 "\
8323sketch(on = XY) {
8324 /* above first - moves to middle */
8325 /* above middle - stays */
8326 point(at = [var 3, var 4])
8327 /* above last - moves to trailing meta */
8328}
8329"
8330 );
8331
8332 ctx.close().await;
8333 mock_ctx.close().await;
8334 }
8335
8336 #[tokio::test(flavor = "multi_thread")]
8337 async fn test_edit_line_when_editing_its_start_point() {
8338 let initial_source = "\
8339sketch(on = XY) {
8340 line(start = [var 1, var 2], end = [var 3, var 4])
8341}
8342";
8343
8344 let program = Program::parse(initial_source).unwrap().0.unwrap();
8345
8346 let mut frontend = FrontendState::new();
8347
8348 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8349 let mock_ctx = ExecutorContext::new_mock(None).await;
8350 let version = Version(0);
8351
8352 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8353 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8354 let sketch_id = sketch_object.id;
8355 let sketch = expect_sketch(sketch_object);
8356
8357 let point_id = *sketch.segments.first().unwrap();
8358
8359 let point_ctor = PointCtor {
8360 position: Point2d {
8361 x: Expr::Var(Number {
8362 value: 5.0,
8363 units: NumericSuffix::Inch,
8364 }),
8365 y: Expr::Var(Number {
8366 value: 6.0,
8367 units: NumericSuffix::Inch,
8368 }),
8369 },
8370 };
8371 let segments = vec![ExistingSegmentCtor {
8372 id: point_id,
8373 ctor: SegmentCtor::Point(point_ctor),
8374 }];
8375 let (src_delta, scene_delta) = frontend
8376 .edit_segments(&mock_ctx, version, sketch_id, segments)
8377 .await
8378 .unwrap();
8379 assert_eq!(
8380 src_delta.text.as_str(),
8381 "\
8382sketch(on = XY) {
8383 line(start = [var 5in, var 6in], end = [var 3, var 4])
8384}
8385"
8386 );
8387 assert_eq!(scene_delta.new_objects, vec![]);
8388 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8389
8390 ctx.close().await;
8391 mock_ctx.close().await;
8392 }
8393
8394 #[tokio::test(flavor = "multi_thread")]
8395 async fn test_edit_line_when_editing_its_end_point() {
8396 let initial_source = "\
8397sketch(on = XY) {
8398 line(start = [var 1, var 2], end = [var 3, var 4])
8399}
8400";
8401
8402 let program = Program::parse(initial_source).unwrap().0.unwrap();
8403
8404 let mut frontend = FrontendState::new();
8405
8406 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8407 let mock_ctx = ExecutorContext::new_mock(None).await;
8408 let version = Version(0);
8409
8410 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8411 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8412 let sketch_id = sketch_object.id;
8413 let sketch = expect_sketch(sketch_object);
8414 let point_id = *sketch.segments.get(1).unwrap();
8415
8416 let point_ctor = PointCtor {
8417 position: Point2d {
8418 x: Expr::Var(Number {
8419 value: 5.0,
8420 units: NumericSuffix::Inch,
8421 }),
8422 y: Expr::Var(Number {
8423 value: 6.0,
8424 units: NumericSuffix::Inch,
8425 }),
8426 },
8427 };
8428 let segments = vec![ExistingSegmentCtor {
8429 id: point_id,
8430 ctor: SegmentCtor::Point(point_ctor),
8431 }];
8432 let (src_delta, scene_delta) = frontend
8433 .edit_segments(&mock_ctx, version, sketch_id, segments)
8434 .await
8435 .unwrap();
8436 assert_eq!(
8437 src_delta.text.as_str(),
8438 "\
8439sketch(on = XY) {
8440 line(start = [var 1, var 2], end = [var 5in, var 6in])
8441}
8442"
8443 );
8444 assert_eq!(scene_delta.new_objects, vec![]);
8445 assert_eq!(
8446 scene_delta.new_graph.objects.len(),
8447 5,
8448 "{:#?}",
8449 scene_delta.new_graph.objects
8450 );
8451
8452 ctx.close().await;
8453 mock_ctx.close().await;
8454 }
8455
8456 #[tokio::test(flavor = "multi_thread")]
8457 async fn test_edit_line_with_coincident_feedback() {
8458 let initial_source = "\
8459sketch(on = XY) {
8460 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8461 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8462 fixed([line1.start, [0, 0]])
8463 coincident([line1.end, line2.start])
8464 equalLength([line1, line2])
8465}
8466";
8467
8468 let program = Program::parse(initial_source).unwrap().0.unwrap();
8469
8470 let mut frontend = FrontendState::new();
8471
8472 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8473 let mock_ctx = ExecutorContext::new_mock(None).await;
8474 let version = Version(0);
8475
8476 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8477 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8478 let sketch_id = sketch_object.id;
8479 let sketch = expect_sketch(sketch_object);
8480 let line2_end_id = *sketch.segments.get(4).unwrap();
8481
8482 let segments = vec![ExistingSegmentCtor {
8483 id: line2_end_id,
8484 ctor: SegmentCtor::Point(PointCtor {
8485 position: Point2d {
8486 x: Expr::Var(Number {
8487 value: 9.0,
8488 units: NumericSuffix::None,
8489 }),
8490 y: Expr::Var(Number {
8491 value: 10.0,
8492 units: NumericSuffix::None,
8493 }),
8494 },
8495 }),
8496 }];
8497 let (src_delta, scene_delta) = frontend
8498 .edit_segments(&mock_ctx, version, sketch_id, segments)
8499 .await
8500 .unwrap();
8501 assert_eq!(
8502 src_delta.text.as_str(),
8503 "\
8504sketch(on = XY) {
8505 line1 = line(start = [var 0, var 0], end = [var 4.14, var 5.32])
8506 line2 = line(start = [var 4.14, var 5.32], end = [var 9, var 10])
8507 fixed([line1.start, [0, 0]])
8508 coincident([line1.end, line2.start])
8509 equalLength([line1, line2])
8510}
8511"
8512 );
8513 assert_eq!(
8514 scene_delta.new_graph.objects.len(),
8515 11,
8516 "{:#?}",
8517 scene_delta.new_graph.objects
8518 );
8519
8520 ctx.close().await;
8521 mock_ctx.close().await;
8522 }
8523
8524 #[tokio::test(flavor = "multi_thread")]
8525 async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
8526 let initial_source = "\
8527sketch(on = XY) {
8528 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8529 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8530 fixed([line1.start, [0, 0]])
8531 coincident([line1.end, line2.start])
8532 equalLength([line1, line2])
8533}
8534";
8535
8536 let program = Program::parse(initial_source).unwrap().0.unwrap();
8537 let mut frontend = FrontendState::new();
8538 let mock_ctx = ExecutorContext::new_mock(None).await;
8539 let version = Version(0);
8540
8541 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8542 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8543 let sketch_id = sketch_object.id;
8544 let sketch = expect_sketch(sketch_object);
8545 let line2_end_id = *sketch.segments.get(4).unwrap();
8546
8547 let segments = vec![ExistingSegmentCtor {
8548 id: line2_end_id,
8549 ctor: SegmentCtor::Point(PointCtor {
8550 position: Point2d {
8551 x: Expr::Var(Number {
8552 value: 9.0,
8553 units: NumericSuffix::None,
8554 }),
8555 y: Expr::Var(Number {
8556 value: 10.0,
8557 units: NumericSuffix::None,
8558 }),
8559 },
8560 }),
8561 }];
8562 let (edited_source, _) = frontend
8563 .edit_segments(&mock_ctx, version, sketch_id, segments)
8564 .await
8565 .unwrap();
8566
8567 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8568 assert_eq!(mock_source.text, edited_source.text);
8569
8570 mock_ctx.close().await;
8571 }
8572
8573 #[tokio::test(flavor = "multi_thread")]
8576 async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
8577 let initial_source = "\
8578sketch(on = XY) {
8579 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8580 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8581 fixed([line1.start, [0, 0]])
8582 coincident([line1.end, line2.start])
8583 equalLength([line1, line2])
8584}
8585";
8586
8587 let program = Program::parse(initial_source).unwrap().0.unwrap();
8588 let mut frontend = FrontendState::new();
8589 let mock_ctx = ExecutorContext::new_mock(None).await;
8590 let version = Version(0);
8591
8592 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8593 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8594 let sketch_id = sketch_object.id;
8595 let sketch = expect_sketch(sketch_object);
8596 let line2_end_id = *sketch.segments.get(4).unwrap();
8597
8598 let segments = vec![ExistingSegmentCtor {
8599 id: line2_end_id,
8600 ctor: SegmentCtor::Point(PointCtor {
8601 position: Point2d {
8602 x: Expr::Var(Number {
8603 value: 9.0,
8604 units: NumericSuffix::None,
8605 }),
8606 y: Expr::Var(Number {
8607 value: 10.0,
8608 units: NumericSuffix::None,
8609 }),
8610 },
8611 }),
8612 }];
8613 let (preview_source, preview_delta) = frontend
8614 .edit_segments_with_options(
8615 &mock_ctx,
8616 version,
8617 sketch_id,
8618 segments,
8619 EditSegmentsOptions {
8620 anchor_segment_ids: Some(vec![line2_end_id]),
8621 drag_anchors: Vec::new(),
8622 commit_solved_initial_guesses: false,
8623 },
8624 )
8625 .await
8626 .unwrap();
8627
8628 assert!(
8629 !preview_delta.exec_outcome.var_solutions.is_empty(),
8630 "preview solve should still solve and return geometry feedback"
8631 );
8632 assert!(
8633 preview_source
8634 .text
8635 .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
8636 );
8637 assert!(
8638 preview_source
8639 .text
8640 .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
8641 );
8642
8643 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8644 assert_eq!(mock_source.text, preview_source.text);
8645
8646 mock_ctx.close().await;
8647 }
8648
8649 #[tokio::test(flavor = "multi_thread")]
8650 async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
8651 let initial_source = "\
8652sketch(on = XY) {
8653 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
8654}
8655";
8656
8657 let program = Program::parse(initial_source).unwrap().0.unwrap();
8658 let mut frontend = FrontendState::new();
8659 let mock_ctx = ExecutorContext::new_mock(None).await;
8660 let version = Version(0);
8661
8662 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8663 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8664 let sketch_id = sketch_object.id;
8665 let sketch = expect_sketch(sketch_object);
8666 let line_end_id = *sketch.segments.get(1).unwrap();
8667
8668 let constraint = Constraint::Fixed(Fixed {
8669 points: vec![FixedPoint {
8670 point: line_end_id,
8671 position: Point2d {
8672 x: Number {
8673 value: 20.0,
8674 units: NumericSuffix::Mm,
8675 },
8676 y: Number {
8677 value: 0.0,
8678 units: NumericSuffix::Mm,
8679 },
8680 },
8681 }],
8682 });
8683 let (constraint_source, _) = frontend
8684 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8685 .await
8686 .unwrap();
8687
8688 assert!(
8689 constraint_source
8690 .text
8691 .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
8692 "{}",
8693 constraint_source.text
8694 );
8695 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8696 assert_eq!(mock_source.text, constraint_source.text);
8697
8698 mock_ctx.close().await;
8699 }
8700
8701 #[test]
8702 fn test_no_solver_feedback_preserves_original_source() {
8703 let initial_source = "\
8704@settings(defaultLengthUnit = in, kclVersion = 2.0)
8705cylinder = startSketchOn(XY)
8706 |> circle(center= [0, 0], radius= 22)
8707 |> extrude(length = 14)
8708";
8709 let mut frontend = FrontendState::new();
8710 frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
8711 let outcome = ExecOutcome {
8712 variables: Default::default(),
8713 operations: Default::default(),
8714 artifact_graph: Default::default(),
8715 scene_objects: Default::default(),
8716 source_range_to_object: Default::default(),
8717 var_solutions: Default::default(),
8718 issues: Default::default(),
8719 filenames: Default::default(),
8720 default_planes: Default::default(),
8721 };
8722
8723 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8724
8725 assert_eq!(source_delta.text, initial_source);
8726 }
8727
8728 #[tokio::test(flavor = "multi_thread")]
8731 async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
8732 let initial_source = "\
8733sketch(on = XY) {
8734 point1 = point(at = [var 0mm, var 0mm])
8735 point2 = point(at = [var 0mm, var 0mm])
8736 coincident([point1, point2])
8737}
8738";
8739
8740 let program = Program::parse(initial_source).unwrap().0.unwrap();
8741 let mut frontend = FrontendState::new();
8742 let mock_ctx = ExecutorContext::new_mock(None).await;
8743 let version = Version(0);
8744
8745 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8746 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8747 let sketch_id = sketch_object.id;
8748 let sketch = expect_sketch(sketch_object);
8749 let point1_id = sketch.segments[0];
8750 let point2_id = sketch.segments[1];
8751
8752 let segments = vec![
8753 ExistingSegmentCtor {
8754 id: point1_id,
8755 ctor: SegmentCtor::Point(PointCtor {
8756 position: point_expr_mm(10.0, 0.0),
8757 }),
8758 },
8759 ExistingSegmentCtor {
8760 id: point2_id,
8761 ctor: SegmentCtor::Point(PointCtor {
8762 position: point_expr_mm(100.0, 0.0),
8763 }),
8764 },
8765 ];
8766 let (_, scene_delta) = frontend
8767 .edit_segments_with_options(
8768 &mock_ctx,
8769 version,
8770 sketch_id,
8771 segments,
8772 EditSegmentsOptions {
8773 anchor_segment_ids: Some(vec![point1_id]),
8774 drag_anchors: Vec::new(),
8775 commit_solved_initial_guesses: true,
8776 },
8777 )
8778 .await
8779 .unwrap();
8780
8781 assert_point_position_close(
8782 point_position(&scene_delta.new_graph, point1_id),
8783 point_number_mm(10.0, 0.0),
8784 );
8785 assert_point_position_close(
8786 point_position(&scene_delta.new_graph, point2_id),
8787 point_number_mm(10.0, 0.0),
8788 );
8789
8790 mock_ctx.close().await;
8791 }
8792
8793 fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
8796 use std::cell::RefCell;
8797 struct Collector {
8798 target: f64,
8799 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
8800 }
8801 impl<'a> crate::walk::Visitor<'a> for &Collector {
8802 type Error = crate::front::Error;
8803 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
8804 if let crate::walk::Node::SketchVar(sketch_var) = node
8805 && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
8806 && (initial.value - self.target).abs() < 1e-9
8807 {
8808 self.out
8809 .borrow_mut()
8810 .push((SourceRange::from(initial.as_ref()), node_path.clone()));
8811 }
8812 for child in node.children().iter() {
8813 if !child.visit(*self)? {
8814 return Ok(false);
8815 }
8816 }
8817 Ok(true)
8818 }
8819 }
8820 let collector = Collector {
8821 target: value,
8822 out: Default::default(),
8823 };
8824 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
8825 collector.out.into_inner()
8826 }
8827
8828 fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
8831 use std::cell::RefCell;
8832 struct Collector {
8833 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
8834 }
8835 impl<'a> crate::walk::Visitor<'a> for &Collector {
8836 type Error = crate::front::Error;
8837 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
8838 if let crate::walk::Node::SketchVar(sketch_var) = node
8839 && let Some(node_path) = &sketch_var.node_path
8840 {
8841 self.out
8842 .borrow_mut()
8843 .push((SourceRange::from(sketch_var), node_path.clone()));
8844 }
8845 for child in node.children().iter() {
8846 if !child.visit(*self)? {
8847 return Ok(false);
8848 }
8849 }
8850 Ok(true)
8851 }
8852 }
8853 let collector = Collector {
8854 out: Default::default(),
8855 };
8856 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
8857 collector.out.into_inner()
8858 }
8859
8860 fn empty_exec_outcome_with_var_solutions(
8861 var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
8862 ) -> ExecOutcome {
8863 ExecOutcome {
8864 variables: Default::default(),
8865 operations: Default::default(),
8866 artifact_graph: Default::default(),
8867 scene_objects: Default::default(),
8868 source_range_to_object: Default::default(),
8869 var_solutions,
8870 issues: Default::default(),
8871 filenames: Default::default(),
8872 default_planes: Default::default(),
8873 }
8874 }
8875
8876 #[test]
8880 fn test_commit_var_solution_by_node_path_updates_sketch_var() {
8881 let initial_source = "\
8882sketch(on = XY) {
8883 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
8884}
8885";
8886 let program = Program::parse(initial_source).unwrap().0.unwrap();
8887 let matches = collect_sketch_var_literals_with_value(&program, 10.0);
8888 assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
8889 let (literal_range, node_path) = matches.into_iter().next().unwrap();
8890
8891 let mut frontend = FrontendState::new();
8892 frontend.program = program;
8893
8894 let outcome = empty_exec_outcome_with_var_solutions(vec![(
8895 literal_range,
8896 Some(node_path),
8897 Number {
8898 value: 25.0,
8899 units: NumericSuffix::Mm,
8900 },
8901 )]);
8902
8903 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8904
8905 assert_eq!(
8906 source_delta.text,
8907 "\
8908sketch(on = XY) {
8909 line1 = line(start = [var 0, var 0], end = [var 25mm, var 0])
8910}
8911",
8912 );
8913 }
8914
8915 #[test]
8922 fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
8923 let compact_source = "\
8924sketch(on = XY) {
8925 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
8926}
8927";
8928 let padded_source = "\
8929// added comment\n// added comment\n\nsketch(on = XY) {
8930 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
8931}
8932";
8933 let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
8934 let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
8935
8936 let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
8937 .into_iter()
8938 .next()
8939 .expect("expected `var 10mm` in compact source");
8940 let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
8941 .into_iter()
8942 .next()
8943 .expect("expected `var 10mm` in padded source");
8944
8945 assert_ne!(
8946 compact_match.0, padded_match.0,
8947 "byte offsets must differ for this test to be meaningful"
8948 );
8949 assert_eq!(
8950 compact_match.1, padded_match.1,
8951 "node paths must agree across whitespace; that's the whole point of NodePath",
8952 );
8953
8954 let mut frontend = FrontendState::new();
8955 frontend.program = padded_program;
8956
8957 let outcome = empty_exec_outcome_with_var_solutions(vec![(
8959 compact_match.0,
8960 Some(compact_match.1),
8961 Number {
8962 value: 30.0,
8963 units: NumericSuffix::Mm,
8964 },
8965 )]);
8966
8967 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8968
8969 assert_eq!(
8970 source_delta.text,
8971 "\
8972// added comment
8973// added comment
8974
8975sketch(on = XY) {
8976 line1 = line(start = [var 0, var 0], end = [var 30mm, var 0])
8977}
8978",
8979 );
8980 }
8981
8982 #[test]
8986 fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
8987 let initial_source = "\
8988sketch(on = XY) {
8989 line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
8990}
8991";
8992 let program = Program::parse(initial_source).unwrap().0.unwrap();
8993
8994 let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
8995 .into_iter()
8996 .next()
8997 .expect("expected `var 10mm`");
8998 let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
8999 .into_iter()
9000 .next()
9001 .expect("expected `var 20mm`");
9002
9003 let mut frontend = FrontendState::new();
9004 frontend.program = program;
9005
9006 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9008 var_20.0,
9009 Some(var_10.1),
9010 Number {
9011 value: 33.0,
9012 units: NumericSuffix::Mm,
9013 },
9014 )]);
9015
9016 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9017
9018 assert_eq!(
9019 source_delta.text,
9020 "\
9021sketch(on = XY) {
9022 line1 = line(start = [var 33mm, var 0mm], end = [var 20mm, var 0mm])
9023}
9024",
9025 );
9026 }
9027
9028 #[test]
9035 fn test_commit_var_solution_writes_back_into_bare_var() {
9036 let initial_source = "\
9037@settings(experimentalFeatures = allow, kclVersion = 2.0)
9038sketch(on = XY) {
9039 line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9040}
9041";
9042 let program = Program::parse(initial_source).unwrap().0.unwrap();
9043
9044 let bare = collect_all_sketch_vars(&program)
9047 .into_iter()
9048 .find(|(range, _)| {
9049 range.end() - range.start() == 3
9051 })
9052 .expect("expected at least one bare `var`");
9053
9054 let mut frontend = FrontendState::new();
9055 frontend.program = program;
9056
9057 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9058 bare.0,
9059 Some(bare.1),
9060 Number {
9061 value: 7.0,
9062 units: NumericSuffix::Mm,
9063 },
9064 )]);
9065
9066 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9067
9068 assert_eq!(
9072 source_delta.text,
9073 "\
9074@settings(experimentalFeatures = allow, kclVersion = 2.0)
9075
9076sketch(on = XY) {
9077 line1 = line(start = [var 7mm, var 0mm], end = [var 10mm, var 0])
9078}
9079",
9080 );
9081 }
9082
9083 #[tokio::test(flavor = "multi_thread")]
9084 async fn test_delete_point_without_var() {
9085 let initial_source = "\
9086sketch(on = XY) {
9087 point(at = [var 1, var 2])
9088 point(at = [var 3, var 4])
9089 point(at = [var 5, var 6])
9090}
9091";
9092
9093 let program = Program::parse(initial_source).unwrap().0.unwrap();
9094
9095 let mut frontend = FrontendState::new();
9096
9097 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9098 let mock_ctx = ExecutorContext::new_mock(None).await;
9099 let version = Version(0);
9100
9101 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9102 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9103 let sketch_id = sketch_object.id;
9104 let sketch = expect_sketch(sketch_object);
9105
9106 let point_id = *sketch.segments.get(1).unwrap();
9107
9108 let (src_delta, scene_delta) = frontend
9109 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9110 .await
9111 .unwrap();
9112 assert_eq!(
9113 src_delta.text.as_str(),
9114 "\
9115sketch(on = XY) {
9116 point(at = [var 1, var 2])
9117 point(at = [var 5, var 6])
9118}
9119"
9120 );
9121 assert_eq!(scene_delta.new_objects, vec![]);
9122 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9123
9124 ctx.close().await;
9125 mock_ctx.close().await;
9126 }
9127
9128 #[tokio::test(flavor = "multi_thread")]
9129 async fn test_delete_point_with_var() {
9130 let initial_source = "\
9131sketch(on = XY) {
9132 point(at = [var 1, var 2])
9133 point1 = point(at = [var 3, var 4])
9134 point(at = [var 5, var 6])
9135}
9136";
9137
9138 let program = Program::parse(initial_source).unwrap().0.unwrap();
9139
9140 let mut frontend = FrontendState::new();
9141
9142 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9143 let mock_ctx = ExecutorContext::new_mock(None).await;
9144 let version = Version(0);
9145
9146 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9147 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9148 let sketch_id = sketch_object.id;
9149 let sketch = expect_sketch(sketch_object);
9150
9151 let point_id = *sketch.segments.get(1).unwrap();
9152
9153 let (src_delta, scene_delta) = frontend
9154 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9155 .await
9156 .unwrap();
9157 assert_eq!(
9158 src_delta.text.as_str(),
9159 "\
9160sketch(on = XY) {
9161 point(at = [var 1, var 2])
9162 point(at = [var 5, var 6])
9163}
9164"
9165 );
9166 assert_eq!(scene_delta.new_objects, vec![]);
9167 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9168
9169 ctx.close().await;
9170 mock_ctx.close().await;
9171 }
9172
9173 #[tokio::test(flavor = "multi_thread")]
9174 async fn test_delete_multiple_points() {
9175 let initial_source = "\
9176sketch(on = XY) {
9177 point(at = [var 1, var 2])
9178 point1 = point(at = [var 3, var 4])
9179 point(at = [var 5, var 6])
9180}
9181";
9182
9183 let program = Program::parse(initial_source).unwrap().0.unwrap();
9184
9185 let mut frontend = FrontendState::new();
9186
9187 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9188 let mock_ctx = ExecutorContext::new_mock(None).await;
9189 let version = Version(0);
9190
9191 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9192 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9193 let sketch_id = sketch_object.id;
9194
9195 let sketch = expect_sketch(sketch_object);
9196
9197 let point1_id = *sketch.segments.first().unwrap();
9198 let point2_id = *sketch.segments.get(1).unwrap();
9199
9200 let (src_delta, scene_delta) = frontend
9201 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9202 .await
9203 .unwrap();
9204 assert_eq!(
9205 src_delta.text.as_str(),
9206 "\
9207sketch(on = XY) {
9208 point(at = [var 5, var 6])
9209}
9210"
9211 );
9212 assert_eq!(scene_delta.new_objects, vec![]);
9213 assert_eq!(scene_delta.new_graph.objects.len(), 3);
9214
9215 ctx.close().await;
9216 mock_ctx.close().await;
9217 }
9218
9219 #[tokio::test(flavor = "multi_thread")]
9220 async fn test_delete_coincident_constraint() {
9221 let initial_source = "\
9222sketch(on = XY) {
9223 point1 = point(at = [var 1, var 2])
9224 point2 = point(at = [var 3, var 4])
9225 coincident([point1, point2])
9226 point(at = [var 5, var 6])
9227}
9228";
9229
9230 let program = Program::parse(initial_source).unwrap().0.unwrap();
9231
9232 let mut frontend = FrontendState::new();
9233
9234 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9235 let mock_ctx = ExecutorContext::new_mock(None).await;
9236 let version = Version(0);
9237
9238 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9239 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9240 let sketch_id = sketch_object.id;
9241 let sketch = expect_sketch(sketch_object);
9242
9243 let coincident_id = *sketch.constraints.first().unwrap();
9244
9245 let (src_delta, scene_delta) = frontend
9246 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9247 .await
9248 .unwrap();
9249 assert_eq!(
9250 src_delta.text.as_str(),
9251 "\
9252sketch(on = XY) {
9253 point1 = point(at = [var 1, var 2])
9254 point2 = point(at = [var 3, var 4])
9255 point(at = [var 5, var 6])
9256}
9257"
9258 );
9259 assert_eq!(scene_delta.new_objects, vec![]);
9260 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9261
9262 ctx.close().await;
9263 mock_ctx.close().await;
9264 }
9265
9266 #[tokio::test(flavor = "multi_thread")]
9267 async fn test_delete_line_cascades_to_coincident_constraint() {
9268 let initial_source = "\
9269sketch(on = XY) {
9270 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9271 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9272 coincident([line1.end, line2.start])
9273}
9274";
9275
9276 let program = Program::parse(initial_source).unwrap().0.unwrap();
9277
9278 let mut frontend = FrontendState::new();
9279
9280 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9281 let mock_ctx = ExecutorContext::new_mock(None).await;
9282 let version = Version(0);
9283
9284 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9285 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9286 let sketch_id = sketch_object.id;
9287 let sketch = expect_sketch(sketch_object);
9288 let line_id = *sketch.segments.get(5).unwrap();
9289
9290 let (src_delta, scene_delta) = frontend
9291 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9292 .await
9293 .unwrap();
9294 assert_eq!(
9295 src_delta.text.as_str(),
9296 "\
9297sketch(on = XY) {
9298 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9299}
9300"
9301 );
9302 assert_eq!(
9303 scene_delta.new_graph.objects.len(),
9304 5,
9305 "{:#?}",
9306 scene_delta.new_graph.objects
9307 );
9308
9309 ctx.close().await;
9310 mock_ctx.close().await;
9311 }
9312
9313 #[tokio::test(flavor = "multi_thread")]
9314 async fn test_delete_line_cascades_to_distance_constraint() {
9315 let initial_source = "\
9316sketch(on = XY) {
9317 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9318 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9319 distance([line1.end, line2.start]) == 10mm
9320}
9321";
9322
9323 let program = Program::parse(initial_source).unwrap().0.unwrap();
9324
9325 let mut frontend = FrontendState::new();
9326
9327 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9328 let mock_ctx = ExecutorContext::new_mock(None).await;
9329 let version = Version(0);
9330
9331 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9332 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9333 let sketch_id = sketch_object.id;
9334 let sketch = expect_sketch(sketch_object);
9335 let line_id = *sketch.segments.get(5).unwrap();
9336
9337 let (src_delta, scene_delta) = frontend
9338 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9339 .await
9340 .unwrap();
9341 assert_eq!(
9342 src_delta.text.as_str(),
9343 "\
9344sketch(on = XY) {
9345 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9346}
9347"
9348 );
9349 assert_eq!(
9350 scene_delta.new_graph.objects.len(),
9351 5,
9352 "{:#?}",
9353 scene_delta.new_graph.objects
9354 );
9355
9356 ctx.close().await;
9357 mock_ctx.close().await;
9358 }
9359
9360 #[tokio::test(flavor = "multi_thread")]
9361 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9362 let initial_source = "\
9363sketch(on = XY) {
9364 point1 = point(at = [var 1, var 2])
9365 point2 = point(at = [var 3, var 4])
9366 horizontalDistance([point1, point2]) == 10mm
9367}
9368";
9369
9370 let program = Program::parse(initial_source).unwrap().0.unwrap();
9371
9372 let mut frontend = FrontendState::new();
9373
9374 let mock_ctx = ExecutorContext::new_mock(None).await;
9375 let version = Version(0);
9376
9377 frontend.program = program.clone();
9378 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9379 frontend.update_state_after_exec(outcome, true);
9380 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9381 let sketch_id = sketch_object.id;
9382 let sketch = expect_sketch(sketch_object);
9383 let point2_id = *sketch.segments.get(1).unwrap();
9384
9385 let (src_delta, scene_delta) = frontend
9386 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9387 .await
9388 .unwrap();
9389 assert_eq!(
9390 src_delta.text.as_str(),
9391 "\
9392sketch(on = XY) {
9393 point1 = point(at = [var 1, var 2])
9394}
9395"
9396 );
9397 assert_eq!(
9398 scene_delta.new_graph.objects.len(),
9399 3,
9400 "{:#?}",
9401 scene_delta.new_graph.objects
9402 );
9403
9404 mock_ctx.close().await;
9405 }
9406
9407 #[tokio::test(flavor = "multi_thread")]
9408 async fn test_delete_line_cascades_to_fixed_constraint() {
9409 let initial_source = "\
9410sketch(on = XY) {
9411 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9412 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9413 fixed([line1.start, [0, 0]])
9414}
9415";
9416
9417 let program = Program::parse(initial_source).unwrap().0.unwrap();
9418
9419 let mut frontend = FrontendState::new();
9420
9421 let mock_ctx = ExecutorContext::new_mock(None).await;
9422 let version = Version(0);
9423
9424 frontend.program = program.clone();
9425 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9426 frontend.update_state_after_exec(outcome, true);
9427 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9428 let sketch_id = sketch_object.id;
9429 let sketch = expect_sketch(sketch_object);
9430 let line1_id = *sketch.segments.get(2).unwrap();
9431
9432 let (src_delta, scene_delta) = frontend
9433 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9434 .await
9435 .unwrap();
9436 assert_eq!(
9437 src_delta.text.as_str(),
9438 "\
9439sketch(on = XY) {
9440 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9441}
9442"
9443 );
9444 assert_eq!(
9445 scene_delta.new_graph.objects.len(),
9446 5,
9447 "{:#?}",
9448 scene_delta.new_graph.objects
9449 );
9450
9451 mock_ctx.close().await;
9452 }
9453
9454 #[tokio::test(flavor = "multi_thread")]
9455 async fn test_delete_line_cascades_to_midpoint_constraint() {
9456 let initial_source = "\
9457sketch(on = XY) {
9458 point1 = point(at = [var 1, var 2])
9459 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9460 midpoint(line1, point = point1)
9461}
9462";
9463
9464 let program = Program::parse(initial_source).unwrap().0.unwrap();
9465
9466 let mut frontend = FrontendState::new();
9467
9468 let mock_ctx = ExecutorContext::new_mock(None).await;
9469 let version = Version(0);
9470
9471 frontend.program = program.clone();
9472 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9473 frontend.update_state_after_exec(outcome, true);
9474 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9475 let sketch_id = sketch_object.id;
9476 let sketch = expect_sketch(sketch_object);
9477 let line1_id = *sketch.segments.get(3).unwrap();
9478
9479 let (src_delta, scene_delta) = frontend
9480 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9481 .await
9482 .unwrap();
9483 assert_eq!(
9484 src_delta.text.as_str(),
9485 "\
9486sketch(on = XY) {
9487 point1 = point(at = [var 1, var 2])
9488}
9489"
9490 );
9491 assert_eq!(
9492 scene_delta.new_graph.objects.len(),
9493 3,
9494 "{:#?}",
9495 scene_delta.new_graph.objects
9496 );
9497
9498 mock_ctx.close().await;
9499 }
9500
9501 #[tokio::test(flavor = "multi_thread")]
9502 async fn test_delete_point_preserves_multiline_coincident_constraint() {
9503 let initial_source = "\
9504sketch(on = XY) {
9505 point1 = point(at = [var 1, var 2])
9506 point2 = point(at = [var 3, var 4])
9507 point3 = point(at = [var 5, var 6])
9508 coincident([point1, point2, point3])
9509}
9510";
9511
9512 let program = Program::parse(initial_source).unwrap().0.unwrap();
9513
9514 let mut frontend = FrontendState::new();
9515
9516 let mock_ctx = ExecutorContext::new_mock(None).await;
9517 let version = Version(0);
9518
9519 frontend.program = program.clone();
9520 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9521 frontend.update_state_after_exec(outcome, true);
9522 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9523 let sketch_id = sketch_object.id;
9524 let sketch = expect_sketch(sketch_object);
9525 let point3_id = *sketch.segments.get(2).unwrap();
9526
9527 let (src_delta, scene_delta) = frontend
9528 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
9529 .await
9530 .unwrap();
9531 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
9532 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
9533 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
9534 assert!(
9535 src_delta.text.contains("coincident([point1, point2])"),
9536 "{}",
9537 src_delta.text
9538 );
9539
9540 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9541 let sketch = expect_sketch(sketch_object);
9542 assert_eq!(sketch.segments.len(), 2);
9543 assert_eq!(sketch.constraints.len(), 1);
9544
9545 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9546 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9547 panic!("Expected constraint object");
9548 };
9549 let Constraint::Coincident(coincident) = constraint else {
9550 panic!("Expected coincident constraint");
9551 };
9552 assert_eq!(
9553 coincident.segments,
9554 sketch
9555 .segments
9556 .iter()
9557 .copied()
9558 .map(Into::into)
9559 .collect::<Vec<ConstraintSegment>>()
9560 );
9561
9562 mock_ctx.close().await;
9563 }
9564
9565 #[tokio::test(flavor = "multi_thread")]
9566 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
9567 let initial_source = "\
9568sketch(on = XY) {
9569 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9570 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9571 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9572 equalLength([line1, line2, line3])
9573}
9574";
9575
9576 let program = Program::parse(initial_source).unwrap().0.unwrap();
9577
9578 let mut frontend = FrontendState::new();
9579
9580 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9581 let mock_ctx = ExecutorContext::new_mock(None).await;
9582 let version = Version(0);
9583
9584 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9585 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9586 let sketch_id = sketch_object.id;
9587 let sketch = expect_sketch(sketch_object);
9588 let line3_id = *sketch.segments.get(8).unwrap();
9589
9590 let (src_delta, scene_delta) = frontend
9591 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9592 .await
9593 .unwrap();
9594 assert_eq!(
9595 src_delta.text.as_str(),
9596 "\
9597sketch(on = XY) {
9598 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9599 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9600 equalLength([line1, line2])
9601}
9602"
9603 );
9604
9605 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9606 let sketch = expect_sketch(sketch_object);
9607 assert_eq!(sketch.constraints.len(), 1);
9608
9609 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9610 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9611 panic!("Expected constraint object");
9612 };
9613 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
9614 panic!("Expected lines equal length constraint");
9615 };
9616 assert_eq!(lines_equal_length.lines.len(), 2);
9617
9618 ctx.close().await;
9619 mock_ctx.close().await;
9620 }
9621
9622 #[tokio::test(flavor = "multi_thread")]
9623 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
9624 let initial_source = "\
9625sketch(on = XY) {
9626 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9627 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9628 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9629 horizontal([line1.end, line2.start, line3.start])
9630}
9631";
9632
9633 let program = Program::parse(initial_source).unwrap().0.unwrap();
9634
9635 let mut frontend = FrontendState::new();
9636
9637 let mock_ctx = ExecutorContext::new_mock(None).await;
9638 let version = Version(0);
9639
9640 frontend.program = program.clone();
9641 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9642 frontend.update_state_after_exec(outcome, true);
9643 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9644 let sketch_id = sketch_object.id;
9645 let sketch = expect_sketch(sketch_object);
9646 let line1_id = *sketch.segments.get(2).unwrap();
9647
9648 let (src_delta, scene_delta) = frontend
9649 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9650 .await
9651 .unwrap();
9652 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9653 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9654 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9655 assert!(
9656 src_delta.text.contains("horizontal([line2.start, line3.start])"),
9657 "{}",
9658 src_delta.text
9659 );
9660
9661 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9662 let sketch = expect_sketch(sketch_object);
9663 assert_eq!(sketch.constraints.len(), 1);
9664
9665 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9666 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9667 panic!("Expected constraint object");
9668 };
9669 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
9670 panic!("Expected horizontal points constraint");
9671 };
9672 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9673 assert_eq!(*points, remaining_points);
9674
9675 mock_ctx.close().await;
9676 }
9677
9678 #[tokio::test(flavor = "multi_thread")]
9679 async fn test_delete_line_preserves_multiline_vertical_constraint() {
9680 let initial_source = "\
9681sketch(on = XY) {
9682 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9683 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9684 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9685 vertical([line1.end, line2.start, line3.start])
9686}
9687";
9688
9689 let program = Program::parse(initial_source).unwrap().0.unwrap();
9690
9691 let mut frontend = FrontendState::new();
9692
9693 let mock_ctx = ExecutorContext::new_mock(None).await;
9694 let version = Version(0);
9695
9696 frontend.program = program.clone();
9697 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9698 frontend.update_state_after_exec(outcome, true);
9699 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9700 let sketch_id = sketch_object.id;
9701 let sketch = expect_sketch(sketch_object);
9702 let line1_id = *sketch.segments.get(2).unwrap();
9703
9704 let (src_delta, scene_delta) = frontend
9705 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9706 .await
9707 .unwrap();
9708 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9709 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9710 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9711 assert!(
9712 src_delta.text.contains("vertical([line2.start, line3.start])"),
9713 "{}",
9714 src_delta.text
9715 );
9716
9717 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9718 let sketch = expect_sketch(sketch_object);
9719 assert_eq!(sketch.constraints.len(), 1);
9720
9721 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9722 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9723 panic!("Expected constraint object");
9724 };
9725 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
9726 panic!("Expected vertical points constraint");
9727 };
9728 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9729 assert_eq!(*points, remaining_points);
9730
9731 mock_ctx.close().await;
9732 }
9733
9734 #[tokio::test(flavor = "multi_thread")]
9735 async fn test_delete_line_preserves_multiline_coincident_constraint() {
9736 let initial_source = "\
9737sketch(on = XY) {
9738 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9739 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9740 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9741 coincident([line1.end, line2.start, line3.start])
9742}
9743";
9744
9745 let program = Program::parse(initial_source).unwrap().0.unwrap();
9746
9747 let mut frontend = FrontendState::new();
9748
9749 let mock_ctx = ExecutorContext::new_mock(None).await;
9750 let version = Version(0);
9751
9752 frontend.program = program.clone();
9753 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9754 frontend.update_state_after_exec(outcome, true);
9755 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9756 let sketch_id = sketch_object.id;
9757 let sketch = expect_sketch(sketch_object);
9758 let line1_id = *sketch.segments.get(2).unwrap();
9759
9760 let (src_delta, scene_delta) = frontend
9761 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9762 .await
9763 .unwrap();
9764 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9765 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9766 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9767 assert!(
9768 src_delta.text.contains("coincident([line2.start, line3.start])"),
9769 "{}",
9770 src_delta.text
9771 );
9772
9773 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9774 let sketch = expect_sketch(sketch_object);
9775 assert_eq!(sketch.constraints.len(), 1);
9776
9777 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9778 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9779 panic!("Expected constraint object");
9780 };
9781 let Constraint::Coincident(coincident) = constraint else {
9782 panic!("Expected coincident constraint");
9783 };
9784 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9785 assert_eq!(coincident.segments, remaining_segments);
9786
9787 mock_ctx.close().await;
9788 }
9789
9790 #[tokio::test(flavor = "multi_thread")]
9791 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
9792 let initial_source = "\
9793sketch(on = XY) {
9794 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9795 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9796 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9797 equalLength([line1, line2, line3])
9798}
9799";
9800
9801 let program = Program::parse(initial_source).unwrap().0.unwrap();
9802
9803 let mut frontend = FrontendState::new();
9804
9805 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9806 let mock_ctx = ExecutorContext::new_mock(None).await;
9807 let version = Version(0);
9808
9809 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9810 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9811 let sketch_id = sketch_object.id;
9812 let sketch = expect_sketch(sketch_object);
9813 let line2_id = *sketch.segments.get(5).unwrap();
9814 let line3_id = *sketch.segments.get(8).unwrap();
9815
9816 let (src_delta, scene_delta) = frontend
9817 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
9818 .await
9819 .unwrap();
9820 assert_eq!(
9821 src_delta.text.as_str(),
9822 "\
9823sketch(on = XY) {
9824 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9825}
9826"
9827 );
9828
9829 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9830 let sketch = expect_sketch(sketch_object);
9831 assert!(sketch.constraints.is_empty());
9832
9833 ctx.close().await;
9834 mock_ctx.close().await;
9835 }
9836
9837 #[tokio::test(flavor = "multi_thread")]
9838 async fn test_delete_line_preserves_multiline_parallel_constraint() {
9839 let initial_source = "\
9840sketch(on = XY) {
9841 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9842 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9843 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9844 parallel([line1, line2, line3])
9845}
9846";
9847
9848 let program = Program::parse(initial_source).unwrap().0.unwrap();
9849
9850 let mut frontend = FrontendState::new();
9851
9852 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9853 let mock_ctx = ExecutorContext::new_mock(None).await;
9854 let version = Version(0);
9855
9856 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9857 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9858 let sketch_id = sketch_object.id;
9859 let sketch = expect_sketch(sketch_object);
9860 let line3_id = *sketch.segments.get(8).unwrap();
9861
9862 let (src_delta, scene_delta) = frontend
9863 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9864 .await
9865 .unwrap();
9866 assert_eq!(
9867 src_delta.text.as_str(),
9868 "\
9869sketch(on = XY) {
9870 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9871 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9872 parallel([line1, line2])
9873}
9874"
9875 );
9876
9877 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9878 let sketch = expect_sketch(sketch_object);
9879 assert_eq!(sketch.constraints.len(), 1);
9880
9881 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9882 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9883 panic!("Expected constraint object");
9884 };
9885 let Constraint::Parallel(parallel) = constraint else {
9886 panic!("Expected parallel constraint");
9887 };
9888 assert_eq!(parallel.lines.len(), 2);
9889
9890 ctx.close().await;
9891 mock_ctx.close().await;
9892 }
9893
9894 #[tokio::test(flavor = "multi_thread")]
9895 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
9896 let initial_source = "\
9897sketch(on = XY) {
9898 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9899 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9900 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9901 parallel([line1, line2, line3])
9902}
9903";
9904
9905 let program = Program::parse(initial_source).unwrap().0.unwrap();
9906
9907 let mut frontend = FrontendState::new();
9908
9909 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9910 let mock_ctx = ExecutorContext::new_mock(None).await;
9911 let version = Version(0);
9912
9913 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9914 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9915 let sketch_id = sketch_object.id;
9916 let sketch = expect_sketch(sketch_object);
9917 let line2_id = *sketch.segments.get(5).unwrap();
9918 let line3_id = *sketch.segments.get(8).unwrap();
9919
9920 let (src_delta, scene_delta) = frontend
9921 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
9922 .await
9923 .unwrap();
9924 assert_eq!(
9925 src_delta.text.as_str(),
9926 "\
9927sketch(on = XY) {
9928 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9929}
9930"
9931 );
9932
9933 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9934 let sketch = expect_sketch(sketch_object);
9935 assert!(sketch.constraints.is_empty());
9936
9937 ctx.close().await;
9938 mock_ctx.close().await;
9939 }
9940
9941 #[tokio::test(flavor = "multi_thread")]
9942 async fn test_delete_line_line_coincident_constraint() {
9943 let initial_source = "\
9944sketch(on = XY) {
9945 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9946 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9947 coincident([line1, line2])
9948}
9949";
9950
9951 let program = Program::parse(initial_source).unwrap().0.unwrap();
9952
9953 let mut frontend = FrontendState::new();
9954
9955 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9956 let mock_ctx = ExecutorContext::new_mock(None).await;
9957 let version = Version(0);
9958
9959 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9960 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9961 let sketch_id = sketch_object.id;
9962 let sketch = expect_sketch(sketch_object);
9963
9964 let coincident_id = *sketch.constraints.first().unwrap();
9965
9966 let (src_delta, scene_delta) = frontend
9967 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9968 .await
9969 .unwrap();
9970 assert_eq!(
9971 src_delta.text.as_str(),
9972 "\
9973sketch(on = XY) {
9974 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9975 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9976}
9977"
9978 );
9979 assert_eq!(scene_delta.new_objects, vec![]);
9980 assert_eq!(scene_delta.new_graph.objects.len(), 8);
9981
9982 ctx.close().await;
9983 mock_ctx.close().await;
9984 }
9985
9986 #[tokio::test(flavor = "multi_thread")]
9987 async fn test_two_points_coincident() {
9988 let initial_source = "\
9989sketch(on = XY) {
9990 point1 = point(at = [var 1, var 2])
9991 point(at = [3, 4])
9992}
9993";
9994
9995 let program = Program::parse(initial_source).unwrap().0.unwrap();
9996
9997 let mut frontend = FrontendState::new();
9998
9999 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10000 let mock_ctx = ExecutorContext::new_mock(None).await;
10001 let version = Version(0);
10002
10003 frontend.hack_set_program(&ctx, program).await.unwrap();
10004 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10005 let sketch_id = sketch_object.id;
10006 let sketch = expect_sketch(sketch_object);
10007 let point0_id = *sketch.segments.first().unwrap();
10008 let point1_id = *sketch.segments.get(1).unwrap();
10009
10010 let constraint = Constraint::Coincident(Coincident {
10011 segments: vec![point0_id.into(), point1_id.into()],
10012 });
10013 let (src_delta, scene_delta) = frontend
10014 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10015 .await
10016 .unwrap();
10017 assert_eq!(
10018 src_delta.text.as_str(),
10019 "\
10020sketch(on = XY) {
10021 point1 = point(at = [var 3, var 4])
10022 point2 = point(at = [3, 4])
10023 coincident([point1, point2])
10024}
10025"
10026 );
10027 assert_eq!(
10028 scene_delta.new_graph.objects.len(),
10029 5,
10030 "{:#?}",
10031 scene_delta.new_graph.objects
10032 );
10033
10034 ctx.close().await;
10035 mock_ctx.close().await;
10036 }
10037
10038 #[tokio::test(flavor = "multi_thread")]
10039 async fn test_three_points_coincident() {
10040 let initial_source = "\
10041sketch(on = XY) {
10042 point1 = point(at = [var 1, var 2])
10043 point(at = [var 3, var 4])
10044 point(at = [var 5, var 6])
10045}
10046";
10047
10048 let program = Program::parse(initial_source).unwrap().0.unwrap();
10049
10050 let mut frontend = FrontendState::new();
10051
10052 let mock_ctx = ExecutorContext::new_mock(None).await;
10053 let version = Version(0);
10054
10055 frontend.program = program.clone();
10056 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10057 frontend.update_state_after_exec(outcome, true);
10058 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10059 let sketch_id = sketch_object.id;
10060 let sketch = expect_sketch(sketch_object);
10061 let segments = sketch
10062 .segments
10063 .iter()
10064 .take(3)
10065 .copied()
10066 .map(Into::into)
10067 .collect::<Vec<ConstraintSegment>>();
10068
10069 let constraint = Constraint::Coincident(Coincident {
10070 segments: segments.clone(),
10071 });
10072 let (src_delta, scene_delta) = frontend
10073 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10074 .await
10075 .unwrap();
10076 assert_eq!(
10077 src_delta.text.as_str(),
10078 "\
10079sketch(on = XY) {
10080 point1 = point(at = [var 3, var 4])
10081 point2 = point(at = [var 3, var 4])
10082 point3 = point(at = [var 3, var 4])
10083 coincident([point1, point2, point3])
10084}
10085"
10086 );
10087
10088 let constraint_object = scene_delta
10089 .new_graph
10090 .objects
10091 .iter()
10092 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10093 .unwrap();
10094
10095 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10096 panic!("expected a constraint object");
10097 };
10098
10099 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10100
10101 mock_ctx.close().await;
10102 }
10103
10104 #[tokio::test(flavor = "multi_thread")]
10105 async fn test_source_with_three_point_coincident_tracks_all_segments() {
10106 let initial_source = "\
10107sketch(on = XY) {
10108 point1 = point(at = [var 1, var 2])
10109 point2 = point(at = [var 3, var 4])
10110 point3 = point(at = [var 5, var 6])
10111 coincident([point1, point2, point3])
10112}
10113";
10114
10115 let program = Program::parse(initial_source).unwrap().0.unwrap();
10116
10117 let mut frontend = FrontendState::new();
10118
10119 let ctx = ExecutorContext::new_mock(None).await;
10120 frontend.program = program.clone();
10121 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10122 frontend.update_state_after_exec(outcome, true);
10123
10124 let constraint_object = frontend
10125 .scene_graph
10126 .objects
10127 .iter()
10128 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10129 .unwrap();
10130 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10131 panic!("expected a constraint object");
10132 };
10133
10134 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10135 let sketch = expect_sketch(sketch_object);
10136 let expected_segments = sketch
10137 .segments
10138 .iter()
10139 .take(3)
10140 .copied()
10141 .map(Into::into)
10142 .collect::<Vec<ConstraintSegment>>();
10143
10144 assert_eq!(
10145 constraint,
10146 &Constraint::Coincident(Coincident {
10147 segments: expected_segments,
10148 })
10149 );
10150
10151 ctx.close().await;
10152 }
10153
10154 #[tokio::test(flavor = "multi_thread")]
10155 async fn test_point_origin_coincident_preserves_order() {
10156 let initial_source = "\
10157sketch(on = XY) {
10158 point(at = [var 1, var 2])
10159}
10160";
10161
10162 for (origin_first, expected_source) in [
10163 (
10164 true,
10165 "\
10166sketch(on = XY) {
10167 point1 = point(at = [var 0, var 0])
10168 coincident([ORIGIN, point1])
10169}
10170",
10171 ),
10172 (
10173 false,
10174 "\
10175sketch(on = XY) {
10176 point1 = point(at = [var 0, var 0])
10177 coincident([point1, ORIGIN])
10178}
10179",
10180 ),
10181 ] {
10182 let program = Program::parse(initial_source).unwrap().0.unwrap();
10183
10184 let mut frontend = FrontendState::new();
10185
10186 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10187 let mock_ctx = ExecutorContext::new_mock(None).await;
10188 let version = Version(0);
10189
10190 frontend.hack_set_program(&ctx, program).await.unwrap();
10191 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10192 let sketch_id = sketch_object.id;
10193 let sketch = expect_sketch(sketch_object);
10194 let point_id = *sketch.segments.first().unwrap();
10195
10196 let segments = if origin_first {
10197 vec![ConstraintSegment::ORIGIN, point_id.into()]
10198 } else {
10199 vec![point_id.into(), ConstraintSegment::ORIGIN]
10200 };
10201 let constraint = Constraint::Coincident(Coincident {
10202 segments: segments.clone(),
10203 });
10204 let (src_delta, scene_delta) = frontend
10205 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10206 .await
10207 .unwrap();
10208 assert_eq!(src_delta.text.as_str(), expected_source);
10209
10210 let constraint_object = scene_delta
10211 .new_graph
10212 .objects
10213 .iter()
10214 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10215 .unwrap();
10216
10217 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10218 panic!("expected a constraint object");
10219 };
10220
10221 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10222
10223 ctx.close().await;
10224 mock_ctx.close().await;
10225 }
10226 }
10227
10228 #[tokio::test(flavor = "multi_thread")]
10229 async fn test_coincident_of_line_end_points() {
10230 let initial_source = "\
10231sketch(on = XY) {
10232 line(start = [var 1, var 2], end = [var 3, var 4])
10233 line(start = [var 5, var 6], end = [var 7, var 8])
10234}
10235";
10236
10237 let program = Program::parse(initial_source).unwrap().0.unwrap();
10238
10239 let mut frontend = FrontendState::new();
10240
10241 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10242 let mock_ctx = ExecutorContext::new_mock(None).await;
10243 let version = Version(0);
10244
10245 frontend.hack_set_program(&ctx, program).await.unwrap();
10246 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10247 let sketch_id = sketch_object.id;
10248 let sketch = expect_sketch(sketch_object);
10249 let point0_id = *sketch.segments.get(1).unwrap();
10250 let point1_id = *sketch.segments.get(3).unwrap();
10251
10252 let constraint = Constraint::Coincident(Coincident {
10253 segments: vec![point0_id.into(), point1_id.into()],
10254 });
10255 let (src_delta, scene_delta) = frontend
10256 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10257 .await
10258 .unwrap();
10259 assert_eq!(
10260 src_delta.text.as_str(),
10261 "\
10262sketch(on = XY) {
10263 line1 = line(start = [var 1, var 2], end = [var 4, var 5])
10264 line2 = line(start = [var 4, var 5], end = [var 7, var 8])
10265 coincident([line1.end, line2.start])
10266}
10267"
10268 );
10269 assert_eq!(
10270 scene_delta.new_graph.objects.len(),
10271 9,
10272 "{:#?}",
10273 scene_delta.new_graph.objects
10274 );
10275
10276 ctx.close().await;
10277 mock_ctx.close().await;
10278 }
10279
10280 #[tokio::test(flavor = "multi_thread")]
10281 async fn test_coincident_of_line_point_and_circle_segment() {
10282 let initial_source = "\
10283sketch(on = XY) {
10284 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10285 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10286}
10287";
10288 let program = Program::parse(initial_source).unwrap().0.unwrap();
10289 let mut frontend = FrontendState::new();
10290
10291 let mock_ctx = ExecutorContext::new_mock(None).await;
10292 let version = Version(0);
10293
10294 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10295 frontend.program = program;
10296 frontend.update_state_after_exec(outcome, true);
10297 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10298 let sketch_id = sketch_object.id;
10299 let sketch = expect_sketch(sketch_object);
10300
10301 let circle_id = sketch
10302 .segments
10303 .iter()
10304 .copied()
10305 .find(|seg_id| {
10306 matches!(
10307 &frontend.scene_graph.objects[seg_id.0].kind,
10308 ObjectKind::Segment {
10309 segment: Segment::Circle(_)
10310 }
10311 )
10312 })
10313 .expect("Expected a circle segment in sketch");
10314 let line_id = frontend
10315 .scene_graph
10316 .objects
10317 .iter()
10318 .find_map(|obj| match &obj.kind {
10319 ObjectKind::Segment {
10320 segment: Segment::Line(line),
10321 } if line.owner.is_none() => Some(obj.id),
10322 _ => None,
10323 })
10324 .expect("Expected a standalone line segment in scene graph");
10325
10326 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10327 ObjectKind::Segment {
10328 segment: Segment::Line(line),
10329 } => line.start,
10330 _ => panic!("Expected line segment object"),
10331 };
10332
10333 let constraint = Constraint::Coincident(Coincident {
10334 segments: vec![line_start_point_id.into(), circle_id.into()],
10335 });
10336 let (src_delta, _scene_delta) = frontend
10337 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10338 .await
10339 .unwrap();
10340 assert_eq!(
10341 src_delta.text.as_str(),
10342 "\
10343sketch(on = XY) {
10344 circle1 = circle(start = [var 7.02mm, var 0mm], center = [var -0.01mm, var 0.22mm])
10345 line1 = line(start = [var 7mm, var 0.78mm], end = [var 10mm, var 2mm])
10346 coincident([line1.start, circle1])
10347}
10348"
10349 );
10350
10351 mock_ctx.close().await;
10352 }
10353
10354 #[tokio::test(flavor = "multi_thread")]
10355 async fn test_invalid_coincident_arc_and_line_preserves_state() {
10356 let program = Program::empty();
10364
10365 let mut frontend = FrontendState::new();
10366 frontend.program = program;
10367
10368 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10369 let mock_ctx = ExecutorContext::new_mock(None).await;
10370 let version = Version(0);
10371
10372 let sketch_args = SketchCtor {
10373 on: Plane::Default(PlaneName::Xy),
10374 };
10375 let (_src_delta, _scene_delta, sketch_id) = frontend
10376 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10377 .await
10378 .unwrap();
10379
10380 let arc_ctor = ArcCtor {
10382 start: Point2d {
10383 x: Expr::Var(Number {
10384 value: 0.0,
10385 units: NumericSuffix::Mm,
10386 }),
10387 y: Expr::Var(Number {
10388 value: 0.0,
10389 units: NumericSuffix::Mm,
10390 }),
10391 },
10392 end: Point2d {
10393 x: Expr::Var(Number {
10394 value: 10.0,
10395 units: NumericSuffix::Mm,
10396 }),
10397 y: Expr::Var(Number {
10398 value: 10.0,
10399 units: NumericSuffix::Mm,
10400 }),
10401 },
10402 center: Point2d {
10403 x: Expr::Var(Number {
10404 value: 10.0,
10405 units: NumericSuffix::Mm,
10406 }),
10407 y: Expr::Var(Number {
10408 value: 0.0,
10409 units: NumericSuffix::Mm,
10410 }),
10411 },
10412 construction: None,
10413 };
10414 let (_src_delta, scene_delta) = frontend
10415 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10416 .await
10417 .unwrap();
10418 let arc_id = *scene_delta.new_objects.last().unwrap();
10420
10421 let line_ctor = LineCtor {
10423 start: Point2d {
10424 x: Expr::Var(Number {
10425 value: 20.0,
10426 units: NumericSuffix::Mm,
10427 }),
10428 y: Expr::Var(Number {
10429 value: 0.0,
10430 units: NumericSuffix::Mm,
10431 }),
10432 },
10433 end: Point2d {
10434 x: Expr::Var(Number {
10435 value: 30.0,
10436 units: NumericSuffix::Mm,
10437 }),
10438 y: Expr::Var(Number {
10439 value: 10.0,
10440 units: NumericSuffix::Mm,
10441 }),
10442 },
10443 construction: None,
10444 };
10445 let (_src_delta, scene_delta) = frontend
10446 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10447 .await
10448 .unwrap();
10449 let line_id = *scene_delta.new_objects.last().unwrap();
10451
10452 let constraint = Constraint::Coincident(Coincident {
10455 segments: vec![arc_id.into(), line_id.into()],
10456 });
10457 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10458
10459 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10461
10462 let sketch_object_after =
10465 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10466 let sketch_after = expect_sketch(sketch_object_after);
10467
10468 assert!(
10470 sketch_after.segments.contains(&arc_id),
10471 "Arc segment should still exist after failed constraint"
10472 );
10473 assert!(
10474 sketch_after.segments.contains(&line_id),
10475 "Line segment should still exist after failed constraint"
10476 );
10477
10478 let arc_obj = frontend
10480 .scene_graph
10481 .objects
10482 .get(arc_id.0)
10483 .expect("Arc object should still be accessible");
10484 let line_obj = frontend
10485 .scene_graph
10486 .objects
10487 .get(line_id.0)
10488 .expect("Line object should still be accessible");
10489
10490 match &arc_obj.kind {
10493 ObjectKind::Segment {
10494 segment: Segment::Arc(_),
10495 } => {}
10496 _ => panic!("Arc object should still be an arc segment"),
10497 }
10498 match &line_obj.kind {
10499 ObjectKind::Segment {
10500 segment: Segment::Line(_),
10501 } => {}
10502 _ => panic!("Line object should still be a line segment"),
10503 }
10504
10505 ctx.close().await;
10506 mock_ctx.close().await;
10507 }
10508
10509 #[tokio::test(flavor = "multi_thread")]
10510 async fn test_distance_two_points() {
10511 let initial_source = "\
10512sketch(on = XY) {
10513 point(at = [var 1, var 2])
10514 point(at = [var 3, var 4])
10515}
10516";
10517
10518 let program = Program::parse(initial_source).unwrap().0.unwrap();
10519
10520 let mut frontend = FrontendState::new();
10521
10522 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10523 let mock_ctx = ExecutorContext::new_mock(None).await;
10524 let version = Version(0);
10525
10526 frontend.hack_set_program(&ctx, program).await.unwrap();
10527 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10528 let sketch_id = sketch_object.id;
10529 let sketch = expect_sketch(sketch_object);
10530 let point0_id = *sketch.segments.first().unwrap();
10531 let point1_id = *sketch.segments.get(1).unwrap();
10532
10533 let constraint = Constraint::Distance(Distance {
10534 points: vec![point0_id.into(), point1_id.into()],
10535 distance: Number {
10536 value: 2.0,
10537 units: NumericSuffix::Mm,
10538 },
10539 label_position: None,
10540 source: Default::default(),
10541 });
10542 let (src_delta, scene_delta) = frontend
10543 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10544 .await
10545 .unwrap();
10546 assert_eq!(
10547 src_delta.text.as_str(),
10548 "\
10550sketch(on = XY) {
10551 point1 = point(at = [var 1.29, var 2.29])
10552 point2 = point(at = [var 2.71, var 3.71])
10553 distance([point1, point2]) == 2mm
10554}
10555"
10556 );
10557 assert_eq!(
10558 scene_delta.new_graph.objects.len(),
10559 5,
10560 "{:#?}",
10561 scene_delta.new_graph.objects
10562 );
10563
10564 ctx.close().await;
10565 mock_ctx.close().await;
10566 }
10567
10568 #[tokio::test(flavor = "multi_thread")]
10569 async fn test_distance_two_points_with_label() {
10570 let initial_source = "\
10571sketch(on = XY) {
10572 point(at = [var 1, var 2])
10573 point(at = [var 3, var 4])
10574}
10575";
10576
10577 let program = Program::parse(initial_source).unwrap().0.unwrap();
10578
10579 let mut frontend = FrontendState::new();
10580
10581 let mock_ctx = ExecutorContext::new_mock(None).await;
10582 let version = Version(0);
10583
10584 frontend.program = program.clone();
10585 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10586 frontend.update_state_after_exec(outcome, true);
10587 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10588 let sketch_id = sketch_object.id;
10589 let sketch = expect_sketch(sketch_object);
10590 let point0_id = *sketch.segments.first().unwrap();
10591 let point1_id = *sketch.segments.get(1).unwrap();
10592
10593 let label_position = Point2d {
10594 x: Number {
10595 value: 10.0,
10596 units: NumericSuffix::Mm,
10597 },
10598 y: Number {
10599 value: 11.0,
10600 units: NumericSuffix::Mm,
10601 },
10602 };
10603 let constraint = Constraint::Distance(Distance {
10604 points: vec![point0_id.into(), point1_id.into()],
10605 distance: Number {
10606 value: 2.0,
10607 units: NumericSuffix::Mm,
10608 },
10609 label_position: Some(label_position.clone()),
10610 source: Default::default(),
10611 });
10612 let (src_delta, scene_delta) = frontend
10613 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10614 .await
10615 .unwrap();
10616 assert_eq!(
10617 src_delta.text.as_str(),
10618 "\
10619sketch(on = XY) {
10620 point1 = point(at = [var 1.29, var 2.29])
10621 point2 = point(at = [var 2.71, var 3.71])
10622 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10623}
10624"
10625 );
10626
10627 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10628 let sketch = expect_sketch(sketch_object);
10629 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10630 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10631 panic!("Expected constraint object");
10632 };
10633 let Constraint::Distance(distance) = constraint else {
10634 panic!("Expected distance constraint");
10635 };
10636 assert_eq!(distance.label_position, Some(label_position));
10637
10638 mock_ctx.close().await;
10639 }
10640
10641 #[tokio::test(flavor = "multi_thread")]
10642 async fn test_edit_distance_constraint_label_position() {
10643 let initial_source = "\
10644sketch(on = XY) {
10645 point(at = [var 1, var 2])
10646 point(at = [var 3, var 2])
10647}
10648";
10649
10650 let program = Program::parse(initial_source).unwrap().0.unwrap();
10651
10652 let mut frontend = FrontendState::new();
10653
10654 let mock_ctx = ExecutorContext::new_mock(None).await;
10655 let version = Version(0);
10656
10657 frontend.program = program.clone();
10658 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10659 frontend.update_state_after_exec(outcome, true);
10660 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10661 let sketch_id = sketch_object.id;
10662 let sketch = expect_sketch(sketch_object);
10663 let point0_id = *sketch.segments.first().unwrap();
10664 let point1_id = *sketch.segments.get(1).unwrap();
10665
10666 let constraint = Constraint::Distance(Distance {
10667 points: vec![point0_id.into(), point1_id.into()],
10668 distance: Number {
10669 value: 2.0,
10670 units: NumericSuffix::Mm,
10671 },
10672 label_position: None,
10673 source: Default::default(),
10674 });
10675 let (_, scene_delta) = frontend
10676 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10677 .await
10678 .unwrap();
10679 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10680 let sketch = expect_sketch(sketch_object);
10681 let constraint_id = sketch.constraints[0];
10682 let label_position = Point2d {
10683 x: Number {
10684 value: 10.0,
10685 units: NumericSuffix::Mm,
10686 },
10687 y: Number {
10688 value: 11.0,
10689 units: NumericSuffix::Mm,
10690 },
10691 };
10692
10693 let (src_delta, scene_delta) = frontend
10694 .edit_distance_constraint_label_position(
10695 &mock_ctx,
10696 version,
10697 sketch_id,
10698 constraint_id,
10699 label_position.clone(),
10700 vec![],
10701 )
10702 .await
10703 .unwrap();
10704 assert_eq!(
10705 src_delta.text.as_str(),
10706 "\
10707sketch(on = XY) {
10708 point1 = point(at = [var 1, var 2])
10709 point2 = point(at = [var 3, var 2])
10710 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10711}
10712"
10713 );
10714
10715 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10716 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10717 panic!("Expected constraint object");
10718 };
10719 let Constraint::Distance(distance) = constraint else {
10720 panic!("Expected distance constraint");
10721 };
10722 assert_eq!(distance.label_position, Some(label_position));
10723
10724 mock_ctx.close().await;
10725 }
10726
10727 #[tokio::test(flavor = "multi_thread")]
10728 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
10729 let initial_source = "\
10730sketch(on = XY) {
10731 point1 = point(at = [var 0mm, var 0mm])
10732 point2 = point(at = [var 10mm, var 0mm])
10733 distance([point1, point2]) == 5mm
10734}
10735";
10736
10737 let program = Program::parse(initial_source).unwrap().0.unwrap();
10738 let mut frontend = FrontendState::new();
10739 let mock_ctx = ExecutorContext::new_mock(None).await;
10740 let version = Version(0);
10741
10742 frontend.program = program.clone();
10743 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10744 frontend.update_state_after_exec(outcome, true);
10745 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10746 let sketch_id = sketch_object.id;
10747 let sketch = expect_sketch(sketch_object);
10748 let point0_id = sketch.segments[0];
10749 let point1_id = sketch.segments[1];
10750 let constraint_id = sketch.constraints[0];
10751
10752 let edited_segments = vec![ExistingSegmentCtor {
10753 id: point0_id,
10754 ctor: SegmentCtor::Point(PointCtor {
10755 position: Point2d {
10756 x: Expr::Var(Number {
10757 value: 2.0,
10758 units: NumericSuffix::Mm,
10759 }),
10760 y: Expr::Var(Number {
10761 value: 1.0,
10762 units: NumericSuffix::Mm,
10763 }),
10764 },
10765 }),
10766 }];
10767 let (_, scene_delta) = frontend
10768 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
10769 .await
10770 .unwrap();
10771 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
10772 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
10773
10774 let label_position = Point2d {
10775 x: Number {
10776 value: 3.0,
10777 units: NumericSuffix::Mm,
10778 },
10779 y: Number {
10780 value: 4.0,
10781 units: NumericSuffix::Mm,
10782 },
10783 };
10784 let (_, scene_delta) = frontend
10785 .edit_distance_constraint_label_position(
10786 &mock_ctx,
10787 version,
10788 sketch_id,
10789 constraint_id,
10790 label_position,
10791 vec![point0_id],
10792 )
10793 .await
10794 .unwrap();
10795
10796 assert_point_position_close(
10797 point_position(&scene_delta.new_graph, point0_id),
10798 point0_after_segment_edit,
10799 );
10800 assert_point_position_close(
10801 point_position(&scene_delta.new_graph, point1_id),
10802 point1_after_segment_edit,
10803 );
10804
10805 mock_ctx.close().await;
10806 }
10807
10808 #[tokio::test(flavor = "multi_thread")]
10809 async fn test_distance_point_line() {
10810 let initial_source = "\
10811sketch(on = XY) {
10812 point(at = [var 0, var 5])
10813 line(start = [var 0, var 0], end = [var 10, var 0])
10814}
10815";
10816
10817 let program = Program::parse(initial_source).unwrap().0.unwrap();
10818
10819 let mut frontend = FrontendState::new();
10820
10821 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10822 let mock_ctx = ExecutorContext::new_mock(None).await;
10823 let version = Version(0);
10824
10825 frontend.hack_set_program(&ctx, program).await.unwrap();
10826 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10827 let sketch_id = sketch_object.id;
10828 let sketch = expect_sketch(sketch_object);
10829 let point_id = *sketch.segments.first().unwrap();
10830 let line_id = *sketch
10831 .segments
10832 .iter()
10833 .find(|segment_id| {
10834 matches!(
10835 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10836 Some(ObjectKind::Segment {
10837 segment: Segment::Line(_)
10838 })
10839 )
10840 })
10841 .unwrap();
10842
10843 let label_position = Point2d {
10844 x: Number {
10845 value: 10.0,
10846 units: NumericSuffix::Mm,
10847 },
10848 y: Number {
10849 value: 11.0,
10850 units: NumericSuffix::Mm,
10851 },
10852 };
10853 let constraint = Constraint::Distance(Distance {
10854 points: vec![point_id.into(), line_id.into()],
10855 distance: Number {
10856 value: 5.0,
10857 units: NumericSuffix::Mm,
10858 },
10859 label_position: Some(label_position.clone()),
10860 source: Default::default(),
10861 });
10862 let (src_delta, scene_delta) = frontend
10863 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10864 .await
10865 .unwrap();
10866 assert_eq!(
10867 src_delta.text.as_str(),
10868 "\
10869sketch(on = XY) {
10870 point1 = point(at = [var 0, var 5])
10871 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
10872 distance([point1, line1], labelPosition = [10mm, 11mm]) == 5mm
10873}
10874"
10875 );
10876 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10877 let sketch = expect_sketch(sketch_object);
10878 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10879 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10880 panic!("Expected constraint object");
10881 };
10882 let Constraint::Distance(distance) = constraint else {
10883 panic!("Expected distance constraint");
10884 };
10885 assert_eq!(distance.label_position, Some(label_position));
10886
10887 ctx.close().await;
10888 mock_ctx.close().await;
10889 }
10890
10891 #[tokio::test(flavor = "multi_thread")]
10892 async fn test_distance_point_arc() {
10893 let initial_source = "\
10894sketch(on = XY) {
10895 point(at = [var 0, var 8])
10896 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
10897}
10898";
10899
10900 let program = Program::parse(initial_source).unwrap().0.unwrap();
10901
10902 let mut frontend = FrontendState::new();
10903
10904 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10905 let mock_ctx = ExecutorContext::new_mock(None).await;
10906 let version = Version(0);
10907
10908 frontend.hack_set_program(&ctx, program).await.unwrap();
10909 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10910 let sketch_id = sketch_object.id;
10911 let sketch = expect_sketch(sketch_object);
10912 let point_id = *sketch.segments.first().unwrap();
10913 let arc_id = *sketch
10914 .segments
10915 .iter()
10916 .find(|segment_id| {
10917 matches!(
10918 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10919 Some(ObjectKind::Segment {
10920 segment: Segment::Arc(_)
10921 })
10922 )
10923 })
10924 .unwrap();
10925
10926 let constraint = Constraint::Distance(Distance {
10927 points: vec![point_id.into(), arc_id.into()],
10928 distance: Number {
10929 value: 3.0,
10930 units: NumericSuffix::Mm,
10931 },
10932 label_position: None,
10933 source: Default::default(),
10934 });
10935 let (src_delta, _scene_delta) = frontend
10936 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10937 .await
10938 .unwrap();
10939 assert_eq!(
10940 src_delta.text.as_str(),
10941 "\
10942sketch(on = XY) {
10943 point1 = point(at = [var 0, var 8])
10944 arc1 = arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
10945 distance([point1, arc1]) == 3mm
10946}
10947"
10948 );
10949
10950 ctx.close().await;
10951 mock_ctx.close().await;
10952 }
10953
10954 #[tokio::test(flavor = "multi_thread")]
10955 async fn test_distance_arc_origin() {
10956 let initial_source = "\
10957sketch001 = sketch(on = XY) {
10958 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
10959}
10960";
10961
10962 let program = Program::parse(initial_source).unwrap().0.unwrap();
10963
10964 let mut frontend = FrontendState::new();
10965
10966 let mock_ctx = ExecutorContext::new_mock(None).await;
10967 let version = Version(0);
10968
10969 frontend.program = program.clone();
10970 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10971 frontend.update_state_after_exec(outcome, true);
10972 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10973 let sketch_id = sketch_object.id;
10974 let sketch = expect_sketch(sketch_object);
10975 let arc_id = *sketch
10976 .segments
10977 .iter()
10978 .find(|segment_id| {
10979 matches!(
10980 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10981 Some(ObjectKind::Segment {
10982 segment: Segment::Arc(_)
10983 })
10984 )
10985 })
10986 .unwrap();
10987
10988 let constraint = Constraint::Distance(Distance {
10989 points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
10990 distance: Number {
10991 value: 3.0,
10992 units: NumericSuffix::Mm,
10993 },
10994 label_position: None,
10995 source: Default::default(),
10996 });
10997 let (src_delta, _scene_delta) = frontend
10998 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10999 .await
11000 .unwrap();
11001 assert_eq!(
11002 src_delta.text.as_str(),
11003 "\
11004sketch001 = sketch(on = XY) {
11005 arc1 = arc(start = [var -4.16mm, var -0.43mm], end = [var -3.53mm, var 3.28mm], center = [var -4.91mm, var 1.61mm])
11006 distance([arc1, ORIGIN]) == 3mm
11007}
11008"
11009 );
11010
11011 mock_ctx.close().await;
11012 }
11013
11014 #[tokio::test(flavor = "multi_thread")]
11015 async fn test_distance_line_origin() {
11016 let initial_source = "\
11017sketch(on = XY) {
11018 line(start = [var 5, var 0], end = [var 5, var 10])
11019}
11020";
11021
11022 let program = Program::parse(initial_source).unwrap().0.unwrap();
11023
11024 let mut frontend = FrontendState::new();
11025
11026 let mock_ctx = ExecutorContext::new_mock(None).await;
11027 let version = Version(0);
11028
11029 frontend.program = program.clone();
11030 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11031 frontend.update_state_after_exec(outcome, true);
11032 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11033 let sketch_id = sketch_object.id;
11034 let sketch = expect_sketch(sketch_object);
11035 let line_id = *sketch
11036 .segments
11037 .iter()
11038 .find(|segment_id| {
11039 matches!(
11040 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11041 Some(ObjectKind::Segment {
11042 segment: Segment::Line(_)
11043 })
11044 )
11045 })
11046 .unwrap();
11047
11048 let constraint = Constraint::Distance(Distance {
11049 points: vec![ConstraintSegment::ORIGIN, line_id.into()],
11050 distance: Number {
11051 value: 5.0,
11052 units: NumericSuffix::Mm,
11053 },
11054 label_position: None,
11055 source: Default::default(),
11056 });
11057 let (src_delta, _scene_delta) = frontend
11058 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11059 .await
11060 .unwrap();
11061 assert_eq!(
11062 src_delta.text.as_str(),
11063 "\
11064sketch(on = XY) {
11065 line1 = line(start = [var 5, var 0], end = [var 5, var 10])
11066 distance([ORIGIN, line1]) == 5mm
11067}
11068"
11069 );
11070
11071 mock_ctx.close().await;
11072 }
11073
11074 #[tokio::test(flavor = "multi_thread")]
11075 async fn test_distance_line_circle() {
11076 let initial_source = "\
11077sketch(on = XY) {
11078 line(start = [var -10, var 8], end = [var 10, var 8])
11079 circle(start = [var 5, var 0], center = [var 0, var 0])
11080}
11081";
11082
11083 let program = Program::parse(initial_source).unwrap().0.unwrap();
11084
11085 let mut frontend = FrontendState::new();
11086
11087 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11088 let mock_ctx = ExecutorContext::new_mock(None).await;
11089 let version = Version(0);
11090
11091 frontend.hack_set_program(&ctx, program).await.unwrap();
11092 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11093 let sketch_id = sketch_object.id;
11094 let sketch = expect_sketch(sketch_object);
11095 let line_id = *sketch
11096 .segments
11097 .iter()
11098 .find(|segment_id| {
11099 matches!(
11100 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11101 Some(ObjectKind::Segment {
11102 segment: Segment::Line(_)
11103 })
11104 )
11105 })
11106 .unwrap();
11107 let circle_id = *sketch
11108 .segments
11109 .iter()
11110 .find(|segment_id| {
11111 matches!(
11112 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11113 Some(ObjectKind::Segment {
11114 segment: Segment::Circle(_)
11115 })
11116 )
11117 })
11118 .unwrap();
11119
11120 let constraint = Constraint::Distance(Distance {
11121 points: vec![line_id.into(), circle_id.into()],
11122 distance: Number {
11123 value: 3.0,
11124 units: NumericSuffix::Mm,
11125 },
11126 label_position: None,
11127 source: Default::default(),
11128 });
11129 let (src_delta, _scene_delta) = frontend
11130 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11131 .await
11132 .unwrap();
11133 assert_eq!(
11134 src_delta.text.as_str(),
11135 "\
11136sketch(on = XY) {
11137 line1 = line(start = [var -10, var 8], end = [var 10, var 8])
11138 circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
11139 distance([line1, circle1]) == 3mm
11140}
11141"
11142 );
11143
11144 ctx.close().await;
11145 mock_ctx.close().await;
11146 }
11147
11148 #[tokio::test(flavor = "multi_thread")]
11149 async fn test_distance_circle_arc() {
11150 let initial_source = "\
11151sketch(on = XY) {
11152 circle(start = [var 5, var 0], center = [var 0, var 0])
11153 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11154}
11155";
11156
11157 let program = Program::parse(initial_source).unwrap().0.unwrap();
11158
11159 let mut frontend = FrontendState::new();
11160
11161 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11162 let mock_ctx = ExecutorContext::new_mock(None).await;
11163 let version = Version(0);
11164
11165 frontend.hack_set_program(&ctx, program).await.unwrap();
11166 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11167 let sketch_id = sketch_object.id;
11168 let sketch = expect_sketch(sketch_object);
11169 let circle_id = *sketch
11170 .segments
11171 .iter()
11172 .find(|segment_id| {
11173 matches!(
11174 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11175 Some(ObjectKind::Segment {
11176 segment: Segment::Circle(_)
11177 })
11178 )
11179 })
11180 .unwrap();
11181 let arc_id = *sketch
11182 .segments
11183 .iter()
11184 .find(|segment_id| {
11185 matches!(
11186 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11187 Some(ObjectKind::Segment {
11188 segment: Segment::Arc(_)
11189 })
11190 )
11191 })
11192 .unwrap();
11193
11194 let constraint = Constraint::Distance(Distance {
11195 points: vec![circle_id.into(), arc_id.into()],
11196 distance: Number {
11197 value: 3.0,
11198 units: NumericSuffix::Mm,
11199 },
11200 label_position: None,
11201 source: Default::default(),
11202 });
11203 let (src_delta, _scene_delta) = frontend
11204 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11205 .await
11206 .unwrap();
11207 assert_eq!(
11208 src_delta.text.as_str(),
11209 "\
11210sketch(on = XY) {
11211 circle1 = circle(start = [var 4.33, var 0], center = [var -0.34, var -0.09])
11212 arc1 = arc(start = [var 15.33, var -0.01], end = [var 10.01, var 4.33], center = [var 11.34, var 0.53])
11213 distance([circle1, arc1]) == 3mm
11214}
11215"
11216 );
11217
11218 ctx.close().await;
11219 mock_ctx.close().await;
11220 }
11221
11222 #[tokio::test(flavor = "multi_thread")]
11223 async fn test_distance_parallel_lines() {
11224 let initial_source = "\
11225sketch(on = XY) {
11226 line(start = [var 0, var 0], end = [var 10, var 0])
11227 line(start = [var 0, var 5], end = [var 10, var 5])
11228}
11229";
11230
11231 let program = Program::parse(initial_source).unwrap().0.unwrap();
11232
11233 let mut frontend = FrontendState::new();
11234
11235 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11236 let mock_ctx = ExecutorContext::new_mock(None).await;
11237 let version = Version(0);
11238
11239 frontend.hack_set_program(&ctx, program).await.unwrap();
11240 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11241 let sketch_id = sketch_object.id;
11242 let sketch = expect_sketch(sketch_object);
11243 let line_ids = sketch
11244 .segments
11245 .iter()
11246 .copied()
11247 .filter(|segment_id| {
11248 matches!(
11249 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11250 Some(ObjectKind::Segment {
11251 segment: Segment::Line(_)
11252 })
11253 )
11254 })
11255 .collect::<Vec<_>>();
11256
11257 let constraint = Constraint::Distance(Distance {
11258 points: vec![line_ids[0].into(), line_ids[1].into()],
11259 distance: Number {
11260 value: 5.0,
11261 units: NumericSuffix::Mm,
11262 },
11263 label_position: None,
11264 source: Default::default(),
11265 });
11266 let (src_delta, _scene_delta) = frontend
11267 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11268 .await
11269 .unwrap();
11270 assert_eq!(
11271 src_delta.text.as_str(),
11272 "\
11273sketch(on = XY) {
11274 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
11275 line2 = line(start = [var 0, var 5], end = [var 10, var 5])
11276 distance([line1, line2]) == 5mm
11277}
11278"
11279 );
11280
11281 ctx.close().await;
11282 mock_ctx.close().await;
11283 }
11284
11285 #[tokio::test(flavor = "multi_thread")]
11286 async fn test_distance_non_parallel_lines_lowers_to_distance() {
11287 let initial_source = "\
11288sketch(on = XY) {
11289 line(start = [var 0, var 0], end = [var 10, var 0])
11290 line(start = [var 0, var 0], end = [var 0, var 10])
11291}
11292";
11293
11294 let program = Program::parse(initial_source).unwrap().0.unwrap();
11295
11296 let mut frontend = FrontendState::new();
11297
11298 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11299 let mock_ctx = ExecutorContext::new_mock(None).await;
11300 let version = Version(0);
11301
11302 frontend.hack_set_program(&ctx, program).await.unwrap();
11303 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11304 let sketch_id = sketch_object.id;
11305 let sketch = expect_sketch(sketch_object);
11306 let line_ids = sketch
11307 .segments
11308 .iter()
11309 .copied()
11310 .filter(|segment_id| {
11311 matches!(
11312 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11313 Some(ObjectKind::Segment {
11314 segment: Segment::Line(_)
11315 })
11316 )
11317 })
11318 .collect::<Vec<_>>();
11319
11320 let constraint = Constraint::Distance(Distance {
11321 points: vec![line_ids[0].into(), line_ids[1].into()],
11322 distance: Number {
11323 value: 5.0,
11324 units: NumericSuffix::Mm,
11325 },
11326 label_position: None,
11327 source: Default::default(),
11328 });
11329 let (src_delta, _scene_delta) = frontend
11330 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11331 .await
11332 .unwrap();
11333 assert_eq!(
11334 src_delta.text.as_str(),
11335 "\
11336sketch(on = XY) {
11337 line1 = line(start = [var 4.98, var -0.07], end = [var 4.98, var 0.14])
11338 line2 = line(start = [var 0.02, var 4.3], end = [var 0.03, var 5.65])
11339 distance([line1, line2]) == 5mm
11340}
11341"
11342 );
11343
11344 ctx.close().await;
11345 mock_ctx.close().await;
11346 }
11347
11348 #[tokio::test(flavor = "multi_thread")]
11349 async fn test_horizontal_distance_two_points() {
11350 let initial_source = "\
11351sketch(on = XY) {
11352 point(at = [var 1, var 2])
11353 point(at = [var 3, var 4])
11354}
11355";
11356
11357 let program = Program::parse(initial_source).unwrap().0.unwrap();
11358
11359 let mut frontend = FrontendState::new();
11360
11361 let mock_ctx = ExecutorContext::new_mock(None).await;
11362 let version = Version(0);
11363
11364 frontend.program = program.clone();
11365 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11366 frontend.update_state_after_exec(outcome, true);
11367 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11368 let sketch_id = sketch_object.id;
11369 let sketch = expect_sketch(sketch_object);
11370 let point0_id = *sketch.segments.first().unwrap();
11371 let point1_id = *sketch.segments.get(1).unwrap();
11372 let label_position = Point2d {
11373 x: Number {
11374 value: 10.0,
11375 units: NumericSuffix::Mm,
11376 },
11377 y: Number {
11378 value: 11.0,
11379 units: NumericSuffix::Mm,
11380 },
11381 };
11382
11383 let constraint = Constraint::HorizontalDistance(Distance {
11384 points: vec![point0_id.into(), point1_id.into()],
11385 distance: Number {
11386 value: 2.0,
11387 units: NumericSuffix::Mm,
11388 },
11389 label_position: Some(label_position.clone()),
11390 source: Default::default(),
11391 });
11392 let (src_delta, scene_delta) = frontend
11393 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11394 .await
11395 .unwrap();
11396 assert_eq!(
11397 src_delta.text.as_str(),
11398 "\
11400sketch(on = XY) {
11401 point1 = point(at = [var 1, var 2])
11402 point2 = point(at = [var 3, var 4])
11403 horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11404}
11405"
11406 );
11407 assert_eq!(
11408 scene_delta.new_graph.objects.len(),
11409 5,
11410 "{:#?}",
11411 scene_delta.new_graph.objects
11412 );
11413 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11414 let sketch = expect_sketch(sketch_object);
11415 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11416 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11417 panic!("Expected constraint object");
11418 };
11419 let Constraint::HorizontalDistance(distance) = constraint else {
11420 panic!("Expected horizontal distance constraint");
11421 };
11422 assert_eq!(distance.label_position, Some(label_position));
11423
11424 mock_ctx.close().await;
11425 }
11426
11427 #[tokio::test(flavor = "multi_thread")]
11428 async fn test_radius_single_arc_segment() {
11429 let initial_source = "\
11430sketch(on = XY) {
11431 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11432}
11433";
11434
11435 let program = Program::parse(initial_source).unwrap().0.unwrap();
11436
11437 let mut frontend = FrontendState::new();
11438
11439 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11440 let mock_ctx = ExecutorContext::new_mock(None).await;
11441 let version = Version(0);
11442
11443 frontend.hack_set_program(&ctx, program).await.unwrap();
11444 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11445 let sketch_id = sketch_object.id;
11446 let sketch = expect_sketch(sketch_object);
11447 let arc_id = sketch
11449 .segments
11450 .iter()
11451 .find(|&seg_id| {
11452 let obj = frontend.scene_graph.objects.get(seg_id.0);
11453 matches!(
11454 obj.map(|o| &o.kind),
11455 Some(ObjectKind::Segment {
11456 segment: Segment::Arc(_)
11457 })
11458 )
11459 })
11460 .unwrap();
11461
11462 let constraint = Constraint::Radius(Radius {
11463 arc: *arc_id,
11464 radius: Number {
11465 value: 5.0,
11466 units: NumericSuffix::Mm,
11467 },
11468 label_position: None,
11469 source: Default::default(),
11470 });
11471 let (src_delta, scene_delta) = frontend
11472 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11473 .await
11474 .unwrap();
11475 assert_eq!(
11476 src_delta.text.as_str(),
11477 "\
11479sketch(on = XY) {
11480 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11481 radius(arc1) == 5mm
11482}
11483"
11484 );
11485 assert_eq!(
11486 scene_delta.new_graph.objects.len(),
11487 7, "{:#?}",
11489 scene_delta.new_graph.objects
11490 );
11491
11492 ctx.close().await;
11493 mock_ctx.close().await;
11494 }
11495
11496 #[tokio::test(flavor = "multi_thread")]
11497 async fn test_radius_single_arc_segment_with_label_position() {
11498 let initial_source = "\
11499sketch(on = XY) {
11500 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11501}
11502";
11503
11504 let program = Program::parse(initial_source).unwrap().0.unwrap();
11505 let mut frontend = FrontendState::new();
11506 let mock_ctx = ExecutorContext::new_mock(None).await;
11507 let version = Version(0);
11508
11509 frontend.program = program.clone();
11510 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11511 frontend.update_state_after_exec(outcome, true);
11512 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11513 let sketch_id = sketch_object.id;
11514 let sketch = expect_sketch(sketch_object);
11515 let arc_id = sketch
11516 .segments
11517 .iter()
11518 .find(|&seg_id| {
11519 let obj = frontend.scene_graph.objects.get(seg_id.0);
11520 matches!(
11521 obj.map(|o| &o.kind),
11522 Some(ObjectKind::Segment {
11523 segment: Segment::Arc(_)
11524 })
11525 )
11526 })
11527 .unwrap();
11528
11529 let label_position = Point2d {
11530 x: Number {
11531 value: 10.0,
11532 units: NumericSuffix::Mm,
11533 },
11534 y: Number {
11535 value: 11.0,
11536 units: NumericSuffix::Mm,
11537 },
11538 };
11539 let constraint = Constraint::Radius(Radius {
11540 arc: *arc_id,
11541 radius: Number {
11542 value: 5.0,
11543 units: NumericSuffix::Mm,
11544 },
11545 label_position: Some(label_position.clone()),
11546 source: Default::default(),
11547 });
11548 let (src_delta, scene_delta) = frontend
11549 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11550 .await
11551 .unwrap();
11552 assert_eq!(
11553 src_delta.text.as_str(),
11554 "\
11555sketch(on = XY) {
11556 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11557 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11558}
11559"
11560 );
11561
11562 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11563 let sketch = expect_sketch(sketch_object);
11564 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11565 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11566 panic!("Expected constraint object");
11567 };
11568 let Constraint::Radius(radius) = constraint else {
11569 panic!("Expected radius constraint");
11570 };
11571 assert_eq!(radius.label_position, Some(label_position));
11572
11573 mock_ctx.close().await;
11574 }
11575
11576 #[tokio::test(flavor = "multi_thread")]
11577 async fn test_edit_radius_constraint_label_position() {
11578 let initial_source = "\
11579sketch(on = XY) {
11580 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11581 radius(arc1) == 5mm
11582}
11583";
11584
11585 let program = Program::parse(initial_source).unwrap().0.unwrap();
11586 let mut frontend = FrontendState::new();
11587 let mock_ctx = ExecutorContext::new_mock(None).await;
11588 let version = Version(0);
11589
11590 frontend.program = program.clone();
11591 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11592 frontend.update_state_after_exec(outcome, true);
11593 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11594 let sketch_id = sketch_object.id;
11595 let sketch = expect_sketch(sketch_object);
11596 let constraint_id = sketch.constraints[0];
11597 let label_position = Point2d {
11598 x: Number {
11599 value: 10.0,
11600 units: NumericSuffix::Mm,
11601 },
11602 y: Number {
11603 value: 11.0,
11604 units: NumericSuffix::Mm,
11605 },
11606 };
11607
11608 let (src_delta, scene_delta) = frontend
11609 .edit_distance_constraint_label_position(
11610 &mock_ctx,
11611 version,
11612 sketch_id,
11613 constraint_id,
11614 label_position.clone(),
11615 vec![],
11616 )
11617 .await
11618 .unwrap();
11619 assert_eq!(
11620 src_delta.text.as_str(),
11621 "\
11622sketch(on = XY) {
11623 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11624 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11625}
11626"
11627 );
11628
11629 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11630 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11631 panic!("Expected constraint object");
11632 };
11633 let Constraint::Radius(radius) = constraint else {
11634 panic!("Expected radius constraint");
11635 };
11636 assert_eq!(radius.label_position, Some(label_position));
11637
11638 mock_ctx.close().await;
11639 }
11640
11641 #[tokio::test(flavor = "multi_thread")]
11642 async fn test_vertical_distance_two_points() {
11643 let initial_source = "\
11644sketch(on = XY) {
11645 point(at = [var 1, var 2])
11646 point(at = [var 3, var 4])
11647}
11648";
11649
11650 let program = Program::parse(initial_source).unwrap().0.unwrap();
11651
11652 let mut frontend = FrontendState::new();
11653
11654 let mock_ctx = ExecutorContext::new_mock(None).await;
11655 let version = Version(0);
11656
11657 frontend.program = program.clone();
11658 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11659 frontend.update_state_after_exec(outcome, true);
11660 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11661 let sketch_id = sketch_object.id;
11662 let sketch = expect_sketch(sketch_object);
11663 let point0_id = *sketch.segments.first().unwrap();
11664 let point1_id = *sketch.segments.get(1).unwrap();
11665 let label_position = Point2d {
11666 x: Number {
11667 value: 10.0,
11668 units: NumericSuffix::Mm,
11669 },
11670 y: Number {
11671 value: 11.0,
11672 units: NumericSuffix::Mm,
11673 },
11674 };
11675
11676 let constraint = Constraint::VerticalDistance(Distance {
11677 points: vec![point0_id.into(), point1_id.into()],
11678 distance: Number {
11679 value: 2.0,
11680 units: NumericSuffix::Mm,
11681 },
11682 label_position: Some(label_position.clone()),
11683 source: Default::default(),
11684 });
11685 let (src_delta, scene_delta) = frontend
11686 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11687 .await
11688 .unwrap();
11689 assert_eq!(
11690 src_delta.text.as_str(),
11691 "\
11693sketch(on = XY) {
11694 point1 = point(at = [var 1, var 2])
11695 point2 = point(at = [var 3, var 4])
11696 verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11697}
11698"
11699 );
11700 assert_eq!(
11701 scene_delta.new_graph.objects.len(),
11702 5,
11703 "{:#?}",
11704 scene_delta.new_graph.objects
11705 );
11706 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11707 let sketch = expect_sketch(sketch_object);
11708 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11709 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11710 panic!("Expected constraint object");
11711 };
11712 let Constraint::VerticalDistance(distance) = constraint else {
11713 panic!("Expected vertical distance constraint");
11714 };
11715 assert_eq!(distance.label_position, Some(label_position));
11716
11717 mock_ctx.close().await;
11718 }
11719
11720 #[tokio::test(flavor = "multi_thread")]
11721 async fn test_add_fixed_standalone_point() {
11722 let initial_source = "\
11723sketch(on = XY) {
11724 point(at = [var 1, var 2])
11725}
11726";
11727
11728 let program = Program::parse(initial_source).unwrap().0.unwrap();
11729
11730 let mut frontend = FrontendState::new();
11731
11732 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11733 let mock_ctx = ExecutorContext::new_mock(None).await;
11734 let version = Version(0);
11735
11736 frontend.hack_set_program(&ctx, program).await.unwrap();
11737 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11738 let sketch_id = sketch_object.id;
11739 let sketch = expect_sketch(sketch_object);
11740 let point_id = *sketch.segments.first().unwrap();
11741
11742 let (src_delta, scene_delta) = frontend
11743 .add_constraint(
11744 &mock_ctx,
11745 version,
11746 sketch_id,
11747 Constraint::Fixed(Fixed {
11748 points: vec![FixedPoint {
11749 point: point_id,
11750 position: Point2d {
11751 x: Number {
11752 value: 2.0,
11753 units: NumericSuffix::Mm,
11754 },
11755 y: Number {
11756 value: 3.0,
11757 units: NumericSuffix::Mm,
11758 },
11759 },
11760 }],
11761 }),
11762 )
11763 .await
11764 .unwrap();
11765 assert_eq!(
11766 src_delta.text.as_str(),
11767 "\
11768sketch(on = XY) {
11769 point1 = point(at = [var 2, var 3])
11770 fixed([point1, [2mm, 3mm]])
11771}
11772"
11773 );
11774 assert_eq!(
11775 scene_delta.new_graph.objects.len(),
11776 4,
11777 "{:#?}",
11778 scene_delta.new_graph.objects
11779 );
11780
11781 ctx.close().await;
11782 mock_ctx.close().await;
11783 }
11784
11785 #[tokio::test(flavor = "multi_thread")]
11786 async fn test_add_fixed_multiple_points() {
11787 let initial_source = "\
11788sketch(on = XY) {
11789 point(at = [var 1, var 2])
11790 point(at = [var 3, var 4])
11791}
11792";
11793
11794 let program = Program::parse(initial_source).unwrap().0.unwrap();
11795
11796 let mut frontend = FrontendState::new();
11797
11798 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11799 let mock_ctx = ExecutorContext::new_mock(None).await;
11800 let version = Version(0);
11801
11802 frontend.hack_set_program(&ctx, program).await.unwrap();
11803 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11804 let sketch_id = sketch_object.id;
11805 let sketch = expect_sketch(sketch_object);
11806 let point0_id = *sketch.segments.first().unwrap();
11807 let point1_id = *sketch.segments.get(1).unwrap();
11808
11809 let (src_delta, scene_delta) = frontend
11810 .add_constraint(
11811 &mock_ctx,
11812 version,
11813 sketch_id,
11814 Constraint::Fixed(Fixed {
11815 points: vec![
11816 FixedPoint {
11817 point: point0_id,
11818 position: Point2d {
11819 x: Number {
11820 value: 2.0,
11821 units: NumericSuffix::Mm,
11822 },
11823 y: Number {
11824 value: 3.0,
11825 units: NumericSuffix::Mm,
11826 },
11827 },
11828 },
11829 FixedPoint {
11830 point: point1_id,
11831 position: Point2d {
11832 x: Number {
11833 value: 4.0,
11834 units: NumericSuffix::Mm,
11835 },
11836 y: Number {
11837 value: 5.0,
11838 units: NumericSuffix::Mm,
11839 },
11840 },
11841 },
11842 ],
11843 }),
11844 )
11845 .await
11846 .unwrap();
11847 assert_eq!(
11848 src_delta.text.as_str(),
11849 "\
11850sketch(on = XY) {
11851 point1 = point(at = [var 2, var 3])
11852 point2 = point(at = [var 4, var 5])
11853 fixed([point1, [2mm, 3mm]])
11854 fixed([point2, [4mm, 5mm]])
11855}
11856"
11857 );
11858 assert_eq!(
11859 scene_delta.new_graph.objects.len(),
11860 6,
11861 "{:#?}",
11862 scene_delta.new_graph.objects
11863 );
11864
11865 ctx.close().await;
11866 mock_ctx.close().await;
11867 }
11868
11869 #[tokio::test(flavor = "multi_thread")]
11870 async fn test_add_fixed_owned_point() {
11871 let initial_source = "\
11872sketch(on = XY) {
11873 line(start = [var 1, var 2], end = [var 3, var 4])
11874}
11875";
11876
11877 let program = Program::parse(initial_source).unwrap().0.unwrap();
11878
11879 let mut frontend = FrontendState::new();
11880
11881 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11882 let mock_ctx = ExecutorContext::new_mock(None).await;
11883 let version = Version(0);
11884
11885 frontend.hack_set_program(&ctx, program).await.unwrap();
11886 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11887 let sketch_id = sketch_object.id;
11888 let sketch = expect_sketch(sketch_object);
11889 let line_start_id = *sketch.segments.first().unwrap();
11890
11891 let (src_delta, scene_delta) = frontend
11892 .add_constraint(
11893 &mock_ctx,
11894 version,
11895 sketch_id,
11896 Constraint::Fixed(Fixed {
11897 points: vec![FixedPoint {
11898 point: line_start_id,
11899 position: Point2d {
11900 x: Number {
11901 value: 2.0,
11902 units: NumericSuffix::Mm,
11903 },
11904 y: Number {
11905 value: 3.0,
11906 units: NumericSuffix::Mm,
11907 },
11908 },
11909 }],
11910 }),
11911 )
11912 .await
11913 .unwrap();
11914 assert_eq!(
11915 src_delta.text.as_str(),
11916 "\
11917sketch(on = XY) {
11918 line1 = line(start = [var 2, var 3], end = [var 3, var 4])
11919 fixed([line1.start, [2mm, 3mm]])
11920}
11921"
11922 );
11923 assert_eq!(
11924 scene_delta.new_graph.objects.len(),
11925 6,
11926 "{:#?}",
11927 scene_delta.new_graph.objects
11928 );
11929
11930 ctx.close().await;
11931 mock_ctx.close().await;
11932 }
11933
11934 #[tokio::test(flavor = "multi_thread")]
11935 async fn test_radius_error_cases() {
11936 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11937 let mock_ctx = ExecutorContext::new_mock(None).await;
11938 let version = Version(0);
11939
11940 let initial_source_point = "\
11942sketch(on = XY) {
11943 point(at = [var 1, var 2])
11944}
11945";
11946 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
11947 let mut frontend_point = FrontendState::new();
11948 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
11949 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
11950 let sketch_id_point = sketch_object_point.id;
11951 let sketch_point = expect_sketch(sketch_object_point);
11952 let point_id = *sketch_point.segments.first().unwrap();
11953
11954 let constraint_point = Constraint::Radius(Radius {
11955 arc: point_id,
11956 radius: Number {
11957 value: 5.0,
11958 units: NumericSuffix::Mm,
11959 },
11960 label_position: None,
11961 source: Default::default(),
11962 });
11963 let result_point = frontend_point
11964 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
11965 .await;
11966 assert!(result_point.is_err(), "Single point should error for radius");
11967
11968 let initial_source_line = "\
11970sketch(on = XY) {
11971 line(start = [var 1, var 2], end = [var 3, var 4])
11972}
11973";
11974 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
11975 let mut frontend_line = FrontendState::new();
11976 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
11977 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
11978 let sketch_id_line = sketch_object_line.id;
11979 let sketch_line = expect_sketch(sketch_object_line);
11980 let line_id = *sketch_line.segments.first().unwrap();
11981
11982 let constraint_line = Constraint::Radius(Radius {
11983 arc: line_id,
11984 radius: Number {
11985 value: 5.0,
11986 units: NumericSuffix::Mm,
11987 },
11988 label_position: None,
11989 source: Default::default(),
11990 });
11991 let result_line = frontend_line
11992 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
11993 .await;
11994 assert!(result_line.is_err(), "Single line segment should error for radius");
11995
11996 ctx.close().await;
11997 mock_ctx.close().await;
11998 }
11999
12000 #[tokio::test(flavor = "multi_thread")]
12001 async fn test_diameter_single_arc_segment() {
12002 let initial_source = "\
12003sketch(on = XY) {
12004 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12005}
12006";
12007
12008 let program = Program::parse(initial_source).unwrap().0.unwrap();
12009
12010 let mut frontend = FrontendState::new();
12011
12012 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12013 let mock_ctx = ExecutorContext::new_mock(None).await;
12014 let version = Version(0);
12015
12016 frontend.hack_set_program(&ctx, program).await.unwrap();
12017 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12018 let sketch_id = sketch_object.id;
12019 let sketch = expect_sketch(sketch_object);
12020 let arc_id = sketch
12022 .segments
12023 .iter()
12024 .find(|&seg_id| {
12025 let obj = frontend.scene_graph.objects.get(seg_id.0);
12026 matches!(
12027 obj.map(|o| &o.kind),
12028 Some(ObjectKind::Segment {
12029 segment: Segment::Arc(_)
12030 })
12031 )
12032 })
12033 .unwrap();
12034
12035 let constraint = Constraint::Diameter(Diameter {
12036 arc: *arc_id,
12037 diameter: Number {
12038 value: 10.0,
12039 units: NumericSuffix::Mm,
12040 },
12041 label_position: None,
12042 source: Default::default(),
12043 });
12044 let (src_delta, scene_delta) = frontend
12045 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12046 .await
12047 .unwrap();
12048 assert_eq!(
12049 src_delta.text.as_str(),
12050 "\
12052sketch(on = XY) {
12053 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12054 diameter(arc1) == 10mm
12055}
12056"
12057 );
12058 assert_eq!(
12059 scene_delta.new_graph.objects.len(),
12060 7, "{:#?}",
12062 scene_delta.new_graph.objects
12063 );
12064
12065 ctx.close().await;
12066 mock_ctx.close().await;
12067 }
12068
12069 #[tokio::test(flavor = "multi_thread")]
12070 async fn test_diameter_single_arc_segment_with_label_position() {
12071 let initial_source = "\
12072sketch(on = XY) {
12073 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12074}
12075";
12076
12077 let program = Program::parse(initial_source).unwrap().0.unwrap();
12078 let mut frontend = FrontendState::new();
12079 let mock_ctx = ExecutorContext::new_mock(None).await;
12080 let version = Version(0);
12081
12082 frontend.program = program.clone();
12083 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12084 frontend.update_state_after_exec(outcome, true);
12085 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12086 let sketch_id = sketch_object.id;
12087 let sketch = expect_sketch(sketch_object);
12088 let arc_id = sketch
12089 .segments
12090 .iter()
12091 .find(|&seg_id| {
12092 let obj = frontend.scene_graph.objects.get(seg_id.0);
12093 matches!(
12094 obj.map(|o| &o.kind),
12095 Some(ObjectKind::Segment {
12096 segment: Segment::Arc(_)
12097 })
12098 )
12099 })
12100 .unwrap();
12101
12102 let label_position = Point2d {
12103 x: Number {
12104 value: 10.0,
12105 units: NumericSuffix::Mm,
12106 },
12107 y: Number {
12108 value: 11.0,
12109 units: NumericSuffix::Mm,
12110 },
12111 };
12112 let constraint = Constraint::Diameter(Diameter {
12113 arc: *arc_id,
12114 diameter: Number {
12115 value: 10.0,
12116 units: NumericSuffix::Mm,
12117 },
12118 label_position: Some(label_position.clone()),
12119 source: Default::default(),
12120 });
12121 let (src_delta, scene_delta) = frontend
12122 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12123 .await
12124 .unwrap();
12125 assert_eq!(
12126 src_delta.text.as_str(),
12127 "\
12128sketch(on = XY) {
12129 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12130 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12131}
12132"
12133 );
12134
12135 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12136 let sketch = expect_sketch(sketch_object);
12137 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12138 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12139 panic!("Expected constraint object");
12140 };
12141 let Constraint::Diameter(diameter) = constraint else {
12142 panic!("Expected diameter constraint");
12143 };
12144 assert_eq!(diameter.label_position, Some(label_position));
12145
12146 mock_ctx.close().await;
12147 }
12148
12149 #[tokio::test(flavor = "multi_thread")]
12150 async fn test_edit_diameter_constraint_label_position() {
12151 let initial_source = "\
12152sketch(on = XY) {
12153 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12154 diameter(arc1) == 10mm
12155}
12156";
12157
12158 let program = Program::parse(initial_source).unwrap().0.unwrap();
12159 let mut frontend = FrontendState::new();
12160 let mock_ctx = ExecutorContext::new_mock(None).await;
12161 let version = Version(0);
12162
12163 frontend.program = program.clone();
12164 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12165 frontend.update_state_after_exec(outcome, true);
12166 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12167 let sketch_id = sketch_object.id;
12168 let sketch = expect_sketch(sketch_object);
12169 let constraint_id = sketch.constraints[0];
12170 let label_position = Point2d {
12171 x: Number {
12172 value: 10.0,
12173 units: NumericSuffix::Mm,
12174 },
12175 y: Number {
12176 value: 11.0,
12177 units: NumericSuffix::Mm,
12178 },
12179 };
12180
12181 let (src_delta, scene_delta) = frontend
12182 .edit_distance_constraint_label_position(
12183 &mock_ctx,
12184 version,
12185 sketch_id,
12186 constraint_id,
12187 label_position.clone(),
12188 vec![],
12189 )
12190 .await
12191 .unwrap();
12192 assert_eq!(
12193 src_delta.text.as_str(),
12194 "\
12195sketch(on = XY) {
12196 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12197 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12198}
12199"
12200 );
12201
12202 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12203 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12204 panic!("Expected constraint object");
12205 };
12206 let Constraint::Diameter(diameter) = constraint else {
12207 panic!("Expected diameter constraint");
12208 };
12209 assert_eq!(diameter.label_position, Some(label_position));
12210
12211 mock_ctx.close().await;
12212 }
12213
12214 #[tokio::test(flavor = "multi_thread")]
12215 async fn test_diameter_error_cases() {
12216 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12217 let mock_ctx = ExecutorContext::new_mock(None).await;
12218 let version = Version(0);
12219
12220 let initial_source_point = "\
12222sketch(on = XY) {
12223 point(at = [var 1, var 2])
12224}
12225";
12226 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12227 let mut frontend_point = FrontendState::new();
12228 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12229 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12230 let sketch_id_point = sketch_object_point.id;
12231 let sketch_point = expect_sketch(sketch_object_point);
12232 let point_id = *sketch_point.segments.first().unwrap();
12233
12234 let constraint_point = Constraint::Diameter(Diameter {
12235 arc: point_id,
12236 diameter: Number {
12237 value: 10.0,
12238 units: NumericSuffix::Mm,
12239 },
12240 label_position: None,
12241 source: Default::default(),
12242 });
12243 let result_point = frontend_point
12244 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12245 .await;
12246 assert!(result_point.is_err(), "Single point should error for diameter");
12247
12248 let initial_source_line = "\
12250sketch(on = XY) {
12251 line(start = [var 1, var 2], end = [var 3, var 4])
12252}
12253";
12254 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12255 let mut frontend_line = FrontendState::new();
12256 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12257 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12258 let sketch_id_line = sketch_object_line.id;
12259 let sketch_line = expect_sketch(sketch_object_line);
12260 let line_id = *sketch_line.segments.first().unwrap();
12261
12262 let constraint_line = Constraint::Diameter(Diameter {
12263 arc: line_id,
12264 diameter: Number {
12265 value: 10.0,
12266 units: NumericSuffix::Mm,
12267 },
12268 label_position: None,
12269 source: Default::default(),
12270 });
12271 let result_line = frontend_line
12272 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12273 .await;
12274 assert!(result_line.is_err(), "Single line segment should error for diameter");
12275
12276 ctx.close().await;
12277 mock_ctx.close().await;
12278 }
12279
12280 #[tokio::test(flavor = "multi_thread")]
12281 async fn test_line_horizontal() {
12282 let initial_source = "\
12283sketch(on = XY) {
12284 line(start = [var 1, var 2], end = [var 3, var 4])
12285}
12286";
12287
12288 let program = Program::parse(initial_source).unwrap().0.unwrap();
12289
12290 let mut frontend = FrontendState::new();
12291
12292 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12293 let mock_ctx = ExecutorContext::new_mock(None).await;
12294 let version = Version(0);
12295
12296 frontend.hack_set_program(&ctx, program).await.unwrap();
12297 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12298 let sketch_id = sketch_object.id;
12299 let sketch = expect_sketch(sketch_object);
12300 let line1_id = *sketch.segments.get(2).unwrap();
12301
12302 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
12303 let (src_delta, scene_delta) = frontend
12304 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12305 .await
12306 .unwrap();
12307 assert_eq!(
12308 src_delta.text.as_str(),
12309 "\
12310sketch(on = XY) {
12311 line1 = line(start = [var 1, var 3], end = [var 3, var 3])
12312 horizontal(line1)
12313}
12314"
12315 );
12316 assert_eq!(
12317 scene_delta.new_graph.objects.len(),
12318 6,
12319 "{:#?}",
12320 scene_delta.new_graph.objects
12321 );
12322
12323 ctx.close().await;
12324 mock_ctx.close().await;
12325 }
12326
12327 #[tokio::test(flavor = "multi_thread")]
12328 async fn test_control_point_spline_edge_horizontal() {
12329 let initial_source = "\
12330@settings(experimentalFeatures = allow)
12331splineSketch = sketch(on = XY) {
12332 controlPointSpline1 = controlPointSpline(points = [
12333 [var 0mm, var 0mm],
12334 [var 10mm, var 20mm],
12335 [var 20mm, var 0mm],
12336 ])
12337}
12338";
12339
12340 let program = Program::parse(initial_source).unwrap().0.unwrap();
12341
12342 let mut frontend = FrontendState::new();
12343
12344 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12345 let mock_ctx = ExecutorContext::new_mock(None).await;
12346 let version = Version(0);
12347
12348 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12349 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12350 let sketch_id = sketch_object.id;
12351 let sketch = expect_sketch(sketch_object);
12352 let spline_id = sketch
12353 .segments
12354 .iter()
12355 .copied()
12356 .find(|seg_id| {
12357 matches!(
12358 &frontend.scene_graph.objects[seg_id.0].kind,
12359 ObjectKind::Segment {
12360 segment: Segment::ControlPointSpline(_)
12361 }
12362 )
12363 })
12364 .expect("Expected a control point spline segment in sketch");
12365 let edge_id = frontend
12366 .scene_graph
12367 .objects
12368 .iter()
12369 .find_map(|obj| match &obj.kind {
12370 ObjectKind::Segment {
12371 segment: Segment::Line(line),
12372 } if line.owner == Some(spline_id) => Some(obj.id),
12373 _ => None,
12374 })
12375 .expect("Expected an owned control-polygon edge");
12376
12377 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
12378 let (src_delta, _) = frontend
12379 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12380 .await
12381 .unwrap();
12382 assert!(
12383 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
12384 "Expected horizontal constraint on spline edge, got: {}",
12385 src_delta.text
12386 );
12387
12388 ctx.close().await;
12389 mock_ctx.close().await;
12390 }
12391
12392 #[tokio::test(flavor = "multi_thread")]
12393 async fn test_control_point_spline_edge_angle() {
12394 let initial_source = "\
12395@settings(experimentalFeatures = allow)
12396splineSketch = sketch(on = XY) {
12397 controlPointSpline1 = controlPointSpline(points = [
12398 [var 0mm, var 0mm],
12399 [var 10mm, var 20mm],
12400 [var 20mm, var 0mm],
12401 ])
12402
12403 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
12404}
12405";
12406
12407 let program = Program::parse(initial_source).unwrap().0.unwrap();
12408
12409 let mut frontend = FrontendState::new();
12410
12411 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12412 let mock_ctx = ExecutorContext::new_mock(None).await;
12413 let version = Version(0);
12414
12415 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12416 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12417 let sketch_id = sketch_object.id;
12418 let sketch = expect_sketch(sketch_object);
12419 let spline_id = sketch
12420 .segments
12421 .iter()
12422 .copied()
12423 .find(|seg_id| {
12424 matches!(
12425 &frontend.scene_graph.objects[seg_id.0].kind,
12426 ObjectKind::Segment {
12427 segment: Segment::ControlPointSpline(_)
12428 }
12429 )
12430 })
12431 .expect("Expected a control point spline segment in sketch");
12432 let edge_id = frontend
12433 .scene_graph
12434 .objects
12435 .iter()
12436 .find_map(|obj| match &obj.kind {
12437 ObjectKind::Segment {
12438 segment: Segment::Line(line),
12439 } if line.owner == Some(spline_id) => Some(obj.id),
12440 _ => None,
12441 })
12442 .expect("Expected an owned control-polygon edge");
12443 let line1_id = frontend
12444 .scene_graph
12445 .objects
12446 .iter()
12447 .find_map(|obj| match &obj.kind {
12448 ObjectKind::Segment {
12449 segment: Segment::Line(line),
12450 } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
12451 _ => None,
12452 })
12453 .or_else(|| {
12454 sketch.segments.iter().copied().find(|seg_id| {
12455 matches!(
12456 &frontend.scene_graph.objects[seg_id.0].kind,
12457 ObjectKind::Segment {
12458 segment: Segment::Line(line),
12459 } if line.owner.is_none()
12460 )
12461 })
12462 })
12463 .expect("Expected a standalone line segment in sketch");
12464
12465 let constraint = Constraint::Angle(Angle {
12466 lines: vec![line1_id, edge_id],
12467 angle: Number {
12468 value: 30.0,
12469 units: NumericSuffix::Deg,
12470 },
12471 source: Default::default(),
12472 });
12473 let (src_delta, _) = frontend
12474 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12475 .await
12476 .unwrap();
12477 assert!(
12478 src_delta
12479 .text
12480 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
12481 "Expected angle constraint on spline edge, got: {}",
12482 src_delta.text
12483 );
12484
12485 ctx.close().await;
12486 mock_ctx.close().await;
12487 }
12488
12489 #[tokio::test(flavor = "multi_thread")]
12490 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
12491 let initial_source = "\
12492@settings(experimentalFeatures = allow)
12493splineSketch = sketch(on = XY) {
12494 spline1 = controlPointSpline(points = [
12495 [var 0mm, var 0mm],
12496 [var 10mm, var 20mm],
12497 [var 20mm, var 0mm],
12498 ])
12499 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
12500 coincident([spline1.controls[1], spline1.edges[0]])
12501 coincident([spline1.controls[0], line1])
12502}
12503";
12504
12505 let program = Program::parse(initial_source).unwrap().0.unwrap();
12506
12507 let mut frontend = FrontendState::new();
12508
12509 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12510 let mock_ctx = ExecutorContext::new_mock(None).await;
12511
12512 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12513
12514 let ui_scene_graph = frontend.scene_graph_for_ui();
12515 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
12516 let sketch = expect_sketch(sketch_object);
12517
12518 assert_eq!(
12519 sketch.constraints.len(),
12520 1,
12521 "Expected only the external coincident constraint to remain visible in the UI scene graph"
12522 );
12523
12524 let visible_constraints = ui_scene_graph
12525 .objects
12526 .iter()
12527 .filter_map(|object| match &object.kind {
12528 ObjectKind::Constraint {
12529 constraint: Constraint::Coincident(coincident),
12530 } => Some(coincident.clone()),
12531 _ => None,
12532 })
12533 .collect::<Vec<_>>();
12534
12535 assert_eq!(
12536 visible_constraints.len(),
12537 1,
12538 "Expected only one coincident constraint object in the UI scene graph"
12539 );
12540 assert_eq!(
12541 visible_constraints[0].get_segments().len(),
12542 2,
12543 "Expected the remaining visible coincident constraint to reference two segments"
12544 );
12545
12546 ctx.close().await;
12547 mock_ctx.close().await;
12548 }
12549
12550 #[tokio::test(flavor = "multi_thread")]
12551 async fn test_edit_control_point_spline_can_append_control_point() {
12552 let initial_source = "\
12553@settings(experimentalFeatures = allow)
12554splineSketch = sketch(on = XY) {
12555 controlPointSpline(points = [
12556 [var 0mm, var 0mm],
12557 [var 10mm, var 20mm],
12558 [var 20mm, var 0mm],
12559 ])
12560}
12561";
12562
12563 let program = Program::parse(initial_source).unwrap().0.unwrap();
12564
12565 let mut frontend = FrontendState::new();
12566
12567 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12568 let mock_ctx = ExecutorContext::new_mock(None).await;
12569 let version = Version(0);
12570
12571 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12572 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12573 let sketch_id = sketch_object.id;
12574 let sketch = expect_sketch(sketch_object);
12575 let spline_id = sketch
12576 .segments
12577 .iter()
12578 .copied()
12579 .find(|seg_id| {
12580 matches!(
12581 &frontend.scene_graph.objects[seg_id.0].kind,
12582 ObjectKind::Segment {
12583 segment: Segment::ControlPointSpline(_)
12584 }
12585 )
12586 })
12587 .expect("Expected a control point spline segment in sketch");
12588
12589 let ctor = ControlPointSplineCtor {
12590 points: vec![
12591 Point2d {
12592 x: Expr::Var(Number {
12593 value: 0.0,
12594 units: NumericSuffix::Mm,
12595 }),
12596 y: Expr::Var(Number {
12597 value: 0.0,
12598 units: NumericSuffix::Mm,
12599 }),
12600 },
12601 Point2d {
12602 x: Expr::Var(Number {
12603 value: 10.0,
12604 units: NumericSuffix::Mm,
12605 }),
12606 y: Expr::Var(Number {
12607 value: 20.0,
12608 units: NumericSuffix::Mm,
12609 }),
12610 },
12611 Point2d {
12612 x: Expr::Var(Number {
12613 value: 20.0,
12614 units: NumericSuffix::Mm,
12615 }),
12616 y: Expr::Var(Number {
12617 value: 0.0,
12618 units: NumericSuffix::Mm,
12619 }),
12620 },
12621 Point2d {
12622 x: Expr::Var(Number {
12623 value: 30.0,
12624 units: NumericSuffix::Mm,
12625 }),
12626 y: Expr::Var(Number {
12627 value: 10.0,
12628 units: NumericSuffix::Mm,
12629 }),
12630 },
12631 ],
12632 construction: None,
12633 };
12634
12635 let segments = vec![ExistingSegmentCtor {
12636 id: spline_id,
12637 ctor: SegmentCtor::ControlPointSpline(ctor),
12638 }];
12639 let (src_delta, scene_delta) = frontend
12640 .edit_segments(&mock_ctx, version, sketch_id, segments)
12641 .await
12642 .unwrap();
12643
12644 assert!(
12645 src_delta.text.contains("[var 30mm, var 10mm]"),
12646 "Expected appended spline control point in source, got: {}",
12647 src_delta.text
12648 );
12649
12650 assert!(
12651 scene_delta.invalidates_ids,
12652 "Expected appending a spline control point to invalidate ids"
12653 );
12654 let updated_spline = scene_delta
12655 .new_graph
12656 .objects
12657 .iter()
12658 .find_map(|obj| match &obj.kind {
12659 ObjectKind::Segment {
12660 segment: Segment::ControlPointSpline(updated_spline),
12661 } if updated_spline.controls.len() == 4 => Some(updated_spline),
12662 _ => None,
12663 })
12664 .expect("Expected edited scene graph to contain a four-point control point spline");
12665 assert_eq!(
12666 updated_spline.controls.len(),
12667 4,
12668 "Expected edited spline to expose four control points"
12669 );
12670
12671 ctx.close().await;
12672 mock_ctx.close().await;
12673 }
12674
12675 #[tokio::test(flavor = "multi_thread")]
12676 async fn test_line_vertical() {
12677 let initial_source = "\
12678sketch(on = XY) {
12679 line(start = [var 1, var 2], end = [var 3, var 4])
12680}
12681";
12682
12683 let program = Program::parse(initial_source).unwrap().0.unwrap();
12684
12685 let mut frontend = FrontendState::new();
12686
12687 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12688 let mock_ctx = ExecutorContext::new_mock(None).await;
12689 let version = Version(0);
12690
12691 frontend.hack_set_program(&ctx, program).await.unwrap();
12692 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12693 let sketch_id = sketch_object.id;
12694 let sketch = expect_sketch(sketch_object);
12695 let line1_id = *sketch.segments.get(2).unwrap();
12696
12697 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
12698 let (src_delta, scene_delta) = frontend
12699 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12700 .await
12701 .unwrap();
12702 assert_eq!(
12703 src_delta.text.as_str(),
12704 "\
12705sketch(on = XY) {
12706 line1 = line(start = [var 2, var 2], end = [var 2, var 4])
12707 vertical(line1)
12708}
12709"
12710 );
12711 assert_eq!(
12712 scene_delta.new_graph.objects.len(),
12713 6,
12714 "{:#?}",
12715 scene_delta.new_graph.objects
12716 );
12717
12718 ctx.close().await;
12719 mock_ctx.close().await;
12720 }
12721
12722 #[tokio::test(flavor = "multi_thread")]
12723 async fn test_points_vertical() {
12724 let initial_source = "\
12725sketch001 = sketch(on = XY) {
12726 p0 = point(at = [var -2.23mm, var 3.1mm])
12727 pf = point(at = [4, 4])
12728}
12729";
12730
12731 let program = Program::parse(initial_source).unwrap().0.unwrap();
12732
12733 let mut frontend = FrontendState::new();
12734
12735 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12736 let mock_ctx = ExecutorContext::new_mock(None).await;
12737 let version = Version(0);
12738
12739 frontend.hack_set_program(&ctx, program).await.unwrap();
12740 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12741 let sketch_id = sketch_object.id;
12742 let sketch = expect_sketch(sketch_object);
12743 let point_ids = vec![
12744 sketch.segments.first().unwrap().to_owned(),
12745 sketch.segments.get(1).unwrap().to_owned(),
12746 ];
12747
12748 let constraint = Constraint::Vertical(Vertical::Points {
12749 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12750 });
12751 let (src_delta, scene_delta) = frontend
12752 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12753 .await
12754 .unwrap();
12755 assert_eq!(
12756 src_delta.text.as_str(),
12757 "\
12758sketch001 = sketch(on = XY) {
12759 p0 = point(at = [var 4mm, var 3.1mm])
12760 pf = point(at = [4, 4])
12761 vertical([p0, pf])
12762}
12763"
12764 );
12765 assert_eq!(
12766 scene_delta.new_graph.objects.len(),
12767 5,
12768 "{:#?}",
12769 scene_delta.new_graph.objects
12770 );
12771
12772 ctx.close().await;
12773 mock_ctx.close().await;
12774 }
12775
12776 #[tokio::test(flavor = "multi_thread")]
12777 async fn test_points_horizontal() {
12778 let initial_source = "\
12779sketch001 = sketch(on = XY) {
12780 p0 = point(at = [var -2.23mm, var 3.1mm])
12781 pf = point(at = [4, 4])
12782}
12783";
12784
12785 let program = Program::parse(initial_source).unwrap().0.unwrap();
12786
12787 let mut frontend = FrontendState::new();
12788
12789 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12790 let mock_ctx = ExecutorContext::new_mock(None).await;
12791 let version = Version(0);
12792
12793 frontend.hack_set_program(&ctx, program).await.unwrap();
12794 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12795 let sketch_id = sketch_object.id;
12796 let sketch = expect_sketch(sketch_object);
12797 let point_ids = vec![
12798 sketch.segments.first().unwrap().to_owned(),
12799 sketch.segments.get(1).unwrap().to_owned(),
12800 ];
12801
12802 let constraint = Constraint::Horizontal(Horizontal::Points {
12803 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12804 });
12805 let (src_delta, scene_delta) = frontend
12806 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12807 .await
12808 .unwrap();
12809 assert_eq!(
12810 src_delta.text.as_str(),
12811 "\
12812sketch001 = sketch(on = XY) {
12813 p0 = point(at = [var -2.23mm, var 4mm])
12814 pf = point(at = [4, 4])
12815 horizontal([p0, pf])
12816}
12817"
12818 );
12819 assert_eq!(
12820 scene_delta.new_graph.objects.len(),
12821 5,
12822 "{:#?}",
12823 scene_delta.new_graph.objects
12824 );
12825
12826 ctx.close().await;
12827 mock_ctx.close().await;
12828 }
12829
12830 #[tokio::test(flavor = "multi_thread")]
12831 async fn test_point_horizontal_with_origin() {
12832 let initial_source = "\
12833sketch001 = sketch(on = XY) {
12834 p0 = point(at = [var -2.23mm, var 3.1mm])
12835}
12836";
12837
12838 let program = Program::parse(initial_source).unwrap().0.unwrap();
12839
12840 let mut frontend = FrontendState::new();
12841
12842 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12843 let mock_ctx = ExecutorContext::new_mock(None).await;
12844 let version = Version(0);
12845
12846 frontend.hack_set_program(&ctx, program).await.unwrap();
12847 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12848 let sketch_id = sketch_object.id;
12849 let sketch = expect_sketch(sketch_object);
12850 let point_id = *sketch.segments.first().unwrap();
12851
12852 let constraint = Constraint::Horizontal(Horizontal::Points {
12853 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
12854 });
12855 let (src_delta, scene_delta) = frontend
12856 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12857 .await
12858 .unwrap();
12859 assert_eq!(
12860 src_delta.text.as_str(),
12861 "\
12862sketch001 = sketch(on = XY) {
12863 p0 = point(at = [var -2.23mm, var 0mm])
12864 horizontal([p0, ORIGIN])
12865}
12866"
12867 );
12868 assert_eq!(
12869 scene_delta.new_graph.objects.len(),
12870 4,
12871 "{:#?}",
12872 scene_delta.new_graph.objects
12873 );
12874
12875 ctx.close().await;
12876 mock_ctx.close().await;
12877 }
12878
12879 #[tokio::test(flavor = "multi_thread")]
12880 async fn test_lines_equal_length() {
12881 let initial_source = "\
12882sketch(on = XY) {
12883 line(start = [var 1, var 2], end = [var 3, var 4])
12884 line(start = [var 5, var 6], end = [var 7, var 8])
12885}
12886";
12887
12888 let program = Program::parse(initial_source).unwrap().0.unwrap();
12889
12890 let mut frontend = FrontendState::new();
12891
12892 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12893 let mock_ctx = ExecutorContext::new_mock(None).await;
12894 let version = Version(0);
12895
12896 frontend.hack_set_program(&ctx, program).await.unwrap();
12897 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12898 let sketch_id = sketch_object.id;
12899 let sketch = expect_sketch(sketch_object);
12900 let line1_id = *sketch.segments.get(2).unwrap();
12901 let line2_id = *sketch.segments.get(5).unwrap();
12902
12903 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
12904 lines: vec![line1_id, line2_id],
12905 });
12906 let (src_delta, scene_delta) = frontend
12907 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12908 .await
12909 .unwrap();
12910 assert_eq!(
12911 src_delta.text.as_str(),
12912 "\
12913sketch(on = XY) {
12914 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12915 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12916 equalLength([line1, line2])
12917}
12918"
12919 );
12920 assert_eq!(
12921 scene_delta.new_graph.objects.len(),
12922 9,
12923 "{:#?}",
12924 scene_delta.new_graph.objects
12925 );
12926
12927 ctx.close().await;
12928 mock_ctx.close().await;
12929 }
12930
12931 #[tokio::test(flavor = "multi_thread")]
12932 async fn test_add_constraint_multi_line_equal_length() {
12933 let initial_source = "\
12934sketch(on = XY) {
12935 line(start = [var 1, var 2], end = [var 3, var 4])
12936 line(start = [var 5, var 6], end = [var 7, var 8])
12937 line(start = [var 9, var 10], end = [var 11, var 12])
12938}
12939";
12940
12941 let program = Program::parse(initial_source).unwrap().0.unwrap();
12942
12943 let mut frontend = FrontendState::new();
12944 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12945 let mock_ctx = ExecutorContext::new_mock(None).await;
12946 let version = Version(0);
12947
12948 frontend.hack_set_program(&ctx, program).await.unwrap();
12949 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12950 let sketch_id = sketch_object.id;
12951 let sketch = expect_sketch(sketch_object);
12952 let line1_id = *sketch.segments.get(2).unwrap();
12953 let line2_id = *sketch.segments.get(5).unwrap();
12954 let line3_id = *sketch.segments.get(8).unwrap();
12955
12956 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
12957 lines: vec![line1_id, line2_id, line3_id],
12958 });
12959 let (src_delta, scene_delta) = frontend
12960 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12961 .await
12962 .unwrap();
12963 assert_eq!(
12964 src_delta.text.as_str(),
12965 "\
12966sketch(on = XY) {
12967 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
12968 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
12969 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
12970 equalLength([line1, line2, line3])
12971}
12972"
12973 );
12974 let constraints = scene_delta
12975 .new_graph
12976 .objects
12977 .iter()
12978 .filter_map(|obj| {
12979 let ObjectKind::Constraint { constraint } = &obj.kind else {
12980 return None;
12981 };
12982 Some(constraint)
12983 })
12984 .collect::<Vec<_>>();
12985
12986 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
12987 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
12988 panic!("expected equal length constraint, got {:?}", constraints[0]);
12989 };
12990 assert_eq!(lines_equal_length.lines.len(), 3);
12991
12992 ctx.close().await;
12993 mock_ctx.close().await;
12994 }
12995
12996 #[tokio::test(flavor = "multi_thread")]
12997 async fn test_lines_parallel() {
12998 let initial_source = "\
12999sketch(on = XY) {
13000 line(start = [var 1, var 2], end = [var 3, var 4])
13001 line(start = [var 5, var 6], end = [var 7, var 8])
13002}
13003";
13004
13005 let program = Program::parse(initial_source).unwrap().0.unwrap();
13006
13007 let mut frontend = FrontendState::new();
13008
13009 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13010 let mock_ctx = ExecutorContext::new_mock(None).await;
13011 let version = Version(0);
13012
13013 frontend.hack_set_program(&ctx, program).await.unwrap();
13014 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13015 let sketch_id = sketch_object.id;
13016 let sketch = expect_sketch(sketch_object);
13017 let line1_id = *sketch.segments.get(2).unwrap();
13018 let line2_id = *sketch.segments.get(5).unwrap();
13019
13020 let constraint = Constraint::Parallel(Parallel {
13021 lines: vec![line1_id, line2_id],
13022 });
13023 let (src_delta, scene_delta) = frontend
13024 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13025 .await
13026 .unwrap();
13027 assert_eq!(
13028 src_delta.text.as_str(),
13029 "\
13030sketch(on = XY) {
13031 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13032 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13033 parallel([line1, line2])
13034}
13035"
13036 );
13037 assert_eq!(
13038 scene_delta.new_graph.objects.len(),
13039 9,
13040 "{:#?}",
13041 scene_delta.new_graph.objects
13042 );
13043
13044 ctx.close().await;
13045 mock_ctx.close().await;
13046 }
13047
13048 #[tokio::test(flavor = "multi_thread")]
13049 async fn test_lines_parallel_multiline() {
13050 let initial_source = "\
13051sketch(on = XY) {
13052 line(start = [var 1, var 2], end = [var 3, var 4])
13053 line(start = [var 5, var 6], end = [var 7, var 8])
13054 line(start = [var 9, var 10], end = [var 11, var 12])
13055}
13056";
13057
13058 let program = Program::parse(initial_source).unwrap().0.unwrap();
13059
13060 let mut frontend = FrontendState::new();
13061
13062 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13063 let mock_ctx = ExecutorContext::new_mock(None).await;
13064 let version = Version(0);
13065
13066 frontend.hack_set_program(&ctx, program).await.unwrap();
13067 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13068 let sketch_id = sketch_object.id;
13069 let sketch = expect_sketch(sketch_object);
13070 let line1_id = *sketch.segments.get(2).unwrap();
13071 let line2_id = *sketch.segments.get(5).unwrap();
13072 let line3_id = *sketch.segments.get(8).unwrap();
13073
13074 let constraint = Constraint::Parallel(Parallel {
13075 lines: vec![line1_id, line2_id, line3_id],
13076 });
13077 let (src_delta, scene_delta) = frontend
13078 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13079 .await
13080 .unwrap();
13081 assert_eq!(
13082 src_delta.text.as_str(),
13083 "\
13084sketch(on = XY) {
13085 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13086 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13087 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
13088 parallel([line1, line2, line3])
13089}
13090"
13091 );
13092
13093 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13094 let sketch = expect_sketch(sketch_object);
13095 assert_eq!(sketch.constraints.len(), 1);
13096
13097 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13098 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13099 panic!("Expected constraint object");
13100 };
13101 let Constraint::Parallel(parallel) = constraint else {
13102 panic!("Expected parallel constraint");
13103 };
13104 assert_eq!(parallel.lines.len(), 3);
13105
13106 ctx.close().await;
13107 mock_ctx.close().await;
13108 }
13109
13110 #[tokio::test(flavor = "multi_thread")]
13111 async fn test_lines_perpendicular() {
13112 let initial_source = "\
13113sketch(on = XY) {
13114 line(start = [var 1, var 2], end = [var 3, var 4])
13115 line(start = [var 5, var 6], end = [var 7, var 8])
13116}
13117";
13118
13119 let program = Program::parse(initial_source).unwrap().0.unwrap();
13120
13121 let mut frontend = FrontendState::new();
13122
13123 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13124 let mock_ctx = ExecutorContext::new_mock(None).await;
13125 let version = Version(0);
13126
13127 frontend.hack_set_program(&ctx, program).await.unwrap();
13128 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13129 let sketch_id = sketch_object.id;
13130 let sketch = expect_sketch(sketch_object);
13131 let line1_id = *sketch.segments.get(2).unwrap();
13132 let line2_id = *sketch.segments.get(5).unwrap();
13133
13134 let constraint = Constraint::Perpendicular(Perpendicular {
13135 lines: vec![line1_id, line2_id],
13136 });
13137 let (src_delta, scene_delta) = frontend
13138 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13139 .await
13140 .unwrap();
13141 assert_eq!(
13142 src_delta.text.as_str(),
13143 "\
13144sketch(on = XY) {
13145 line1 = line(start = [var 2, var 3], end = [var 2, var 3])
13146 line2 = line(start = [var 6, var 7], end = [var 6, var 7])
13147 perpendicular([line1, line2])
13148}
13149"
13150 );
13151 assert_eq!(
13152 scene_delta.new_graph.objects.len(),
13153 9,
13154 "{:#?}",
13155 scene_delta.new_graph.objects
13156 );
13157
13158 ctx.close().await;
13159 mock_ctx.close().await;
13160 }
13161
13162 #[tokio::test(flavor = "multi_thread")]
13163 async fn test_lines_angle() {
13164 let initial_source = "\
13165sketch(on = XY) {
13166 line(start = [var 1, var 2], end = [var 3, var 4])
13167 line(start = [var 5, var 6], end = [var 7, var 8])
13168}
13169";
13170
13171 let program = Program::parse(initial_source).unwrap().0.unwrap();
13172
13173 let mut frontend = FrontendState::new();
13174
13175 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13176 let mock_ctx = ExecutorContext::new_mock(None).await;
13177 let version = Version(0);
13178
13179 frontend.hack_set_program(&ctx, program).await.unwrap();
13180 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13181 let sketch_id = sketch_object.id;
13182 let sketch = expect_sketch(sketch_object);
13183 let line1_id = *sketch.segments.get(2).unwrap();
13184 let line2_id = *sketch.segments.get(5).unwrap();
13185
13186 let constraint = Constraint::Angle(Angle {
13187 lines: vec![line1_id, line2_id],
13188 angle: Number {
13189 value: 30.0,
13190 units: NumericSuffix::Deg,
13191 },
13192 source: Default::default(),
13193 });
13194 let (src_delta, scene_delta) = frontend
13195 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13196 .await
13197 .unwrap();
13198 assert_eq!(
13199 src_delta.text.as_str(),
13200 "\
13202sketch(on = XY) {
13203 line1 = line(start = [var 0.9, var 2.36], end = [var 3.1, var 3.64])
13204 line2 = line(start = [var 5.36, var 5.9], end = [var 6.64, var 8.1])
13205 angle([line1, line2]) == 30deg
13206}
13207"
13208 );
13209 assert_eq!(
13210 scene_delta.new_graph.objects.len(),
13211 9,
13212 "{:#?}",
13213 scene_delta.new_graph.objects
13214 );
13215
13216 ctx.close().await;
13217 mock_ctx.close().await;
13218 }
13219
13220 #[tokio::test(flavor = "multi_thread")]
13221 async fn test_segments_tangent() {
13222 let initial_source = "\
13223sketch(on = XY) {
13224 line(start = [var 1, var 2], end = [var 3, var 4])
13225 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13226}
13227";
13228
13229 let program = Program::parse(initial_source).unwrap().0.unwrap();
13230
13231 let mut frontend = FrontendState::new();
13232
13233 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13234 let mock_ctx = ExecutorContext::new_mock(None).await;
13235 let version = Version(0);
13236
13237 frontend.hack_set_program(&ctx, program).await.unwrap();
13238 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13239 let sketch_id = sketch_object.id;
13240 let sketch = expect_sketch(sketch_object);
13241 let line1_id = *sketch.segments.get(2).unwrap();
13242 let arc1_id = *sketch.segments.get(6).unwrap();
13243
13244 let constraint = Constraint::Tangent(Tangent {
13245 input: vec![line1_id, arc1_id],
13246 });
13247 let (src_delta, scene_delta) = frontend
13248 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13249 .await
13250 .unwrap();
13251 assert_eq!(
13252 src_delta.text.as_str(),
13253 "\
13254sketch(on = XY) {
13255 line1 = line(start = [var 0.84, var 2.13], end = [var 3.82, var 3.27])
13256 arc1 = arc(start = [var 4.51, var 2.03], end = [var 7.05, var 2.02], center = [var 5.78, var 2.55])
13257 tangent([line1, arc1])
13258}
13259"
13260 );
13261 assert_eq!(
13262 scene_delta.new_graph.objects.len(),
13263 10,
13264 "{:#?}",
13265 scene_delta.new_graph.objects
13266 );
13267
13268 ctx.close().await;
13269 mock_ctx.close().await;
13270 }
13271
13272 #[tokio::test(flavor = "multi_thread")]
13273 async fn test_point_midpoint() {
13274 let initial_source = "\
13275sketch(on = XY) {
13276 point(at = [var 1, var 1])
13277 line(start = [var 0, var 0], end = [var 6, var 4])
13278}
13279";
13280
13281 let program = Program::parse(initial_source).unwrap().0.unwrap();
13282
13283 let mut frontend = FrontendState::new();
13284
13285 let ctx = ExecutorContext::new_mock(None).await;
13286 let version = Version(0);
13287
13288 frontend.program = program.clone();
13289 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13290 frontend.update_state_after_exec(outcome, true);
13291 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13292 let sketch_id = sketch_object.id;
13293 let sketch = expect_sketch(sketch_object);
13294 let point_id = *sketch.segments.first().unwrap();
13295 let line_id = *sketch.segments.get(3).unwrap();
13296
13297 let constraint = Constraint::Midpoint(Midpoint {
13298 point: ConstraintSegment::from(point_id),
13299 segment: line_id,
13300 });
13301 let (src_delta, scene_delta) = frontend
13302 .add_constraint(&ctx, version, sketch_id, constraint)
13303 .await
13304 .unwrap();
13305 assert_eq!(
13306 src_delta.text.as_str(),
13307 "\
13308sketch(on = XY) {
13309 point1 = point(at = [var 2.33, var 1.67])
13310 line1 = line(start = [var -0.67, var -0.33], end = [var 5.33, var 3.67])
13311 midpoint(line1, point = point1)
13312}
13313"
13314 );
13315 assert_eq!(
13316 scene_delta.new_graph.objects.len(),
13317 7,
13318 "{:#?}",
13319 scene_delta.new_graph.objects
13320 );
13321
13322 ctx.close().await;
13323 }
13324
13325 #[tokio::test(flavor = "multi_thread")]
13326 async fn test_segments_symmetric() {
13327 let initial_source = "\
13328sketch(on = XY) {
13329 line(start = [var 0, var 0], end = [var 0, var 4])
13330 line(start = [var 4, var 0], end = [var 4, var 4])
13331 line(start = [var 2, var -1], end = [var 2, var 5])
13332}
13333";
13334
13335 let program = Program::parse(initial_source).unwrap().0.unwrap();
13336
13337 let mut frontend = FrontendState::new();
13338
13339 let ctx = ExecutorContext::new_mock(None).await;
13340 let version = Version(0);
13341
13342 frontend.program = program.clone();
13343 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13344 frontend.update_state_after_exec(outcome, true);
13345 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13346 let sketch_id = sketch_object.id;
13347 let sketch = expect_sketch(sketch_object);
13348 let line1_id = *sketch.segments.get(2).unwrap();
13349 let line2_id = *sketch.segments.get(5).unwrap();
13350 let axis_id = *sketch.segments.get(8).unwrap();
13351
13352 let constraint = Constraint::Symmetric(Symmetric {
13353 input: vec![line1_id, line2_id],
13354 axis: axis_id,
13355 });
13356 let (src_delta, scene_delta) = frontend
13357 .add_constraint(&ctx, version, sketch_id, constraint)
13358 .await
13359 .unwrap();
13360 assert_eq!(
13361 src_delta.text.as_str(),
13362 "\
13363sketch(on = XY) {
13364 line1 = line(start = [var 0, var 0], end = [var 0, var 4])
13365 line2 = line(start = [var 4, var 0], end = [var 4, var 4])
13366 line3 = line(start = [var 2, var -1], end = [var 2, var 5])
13367 symmetric([line1, line2], axis = line3)
13368}
13369"
13370 );
13371 assert_eq!(
13372 scene_delta.new_graph.objects.len(),
13373 12,
13374 "{:#?}",
13375 scene_delta.new_graph.objects
13376 );
13377
13378 ctx.close().await;
13379 }
13380
13381 #[tokio::test(flavor = "multi_thread")]
13382 async fn test_point_arc_midpoint() {
13383 let initial_source = "\
13384sketch(on = XY) {
13385 point(at = [var 6, var 3])
13386 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13387}
13388";
13389
13390 let program = Program::parse(initial_source).unwrap().0.unwrap();
13391
13392 let mut frontend = FrontendState::new();
13393
13394 let ctx = ExecutorContext::new_mock(None).await;
13395 let version = Version(0);
13396
13397 frontend.program = program.clone();
13398 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13399 frontend.update_state_after_exec(outcome, true);
13400 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13401 let sketch_id = sketch_object.id;
13402 let sketch = expect_sketch(sketch_object);
13403 let point_id = *sketch.segments.first().unwrap();
13404 let arc_id = *sketch.segments.get(4).unwrap();
13405
13406 let constraint = Constraint::Midpoint(Midpoint {
13407 point: ConstraintSegment::from(point_id),
13408 segment: arc_id,
13409 });
13410 let (src_delta, scene_delta) = frontend
13411 .add_constraint(&ctx, version, sketch_id, constraint)
13412 .await
13413 .unwrap();
13414 assert_eq!(
13415 src_delta.text.as_str(),
13416 "\
13417sketch(on = XY) {
13418 point1 = point(at = [var 6, var 2.35])
13419 arc1 = arc(start = [var 6, var 2.35], end = [var 6, var 2.35], center = [var 6, var 1.94])
13420 midpoint(arc1, point = point1)
13421}
13422"
13423 );
13424 assert_eq!(
13425 scene_delta.new_graph.objects.len(),
13426 8,
13427 "{:#?}",
13428 scene_delta.new_graph.objects
13429 );
13430
13431 ctx.close().await;
13432 }
13433
13434 #[tokio::test(flavor = "multi_thread")]
13435 async fn test_origin_line_midpoint() {
13436 let initial_source = "\
13437sketch(on = XY) {
13438 line(start = [var 0, var 0], end = [var 6, var 4])
13439}
13440";
13441
13442 let program = Program::parse(initial_source).unwrap().0.unwrap();
13443
13444 let mut frontend = FrontendState::new();
13445
13446 let ctx = ExecutorContext::new_mock(None).await;
13447 let version = Version(0);
13448
13449 frontend.program = program.clone();
13450 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13451 frontend.update_state_after_exec(outcome, true);
13452 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13453 let sketch_id = sketch_object.id;
13454 let sketch = expect_sketch(sketch_object);
13455 let line_id = *sketch.segments.get(2).unwrap();
13456
13457 let constraint = Constraint::Midpoint(Midpoint {
13458 point: ConstraintSegment::ORIGIN,
13459 segment: line_id,
13460 });
13461 let (src_delta, scene_delta) = frontend
13462 .add_constraint(&ctx, version, sketch_id, constraint)
13463 .await
13464 .unwrap();
13465 assert_eq!(
13466 src_delta.text.as_str(),
13467 "\
13468sketch(on = XY) {
13469 line1 = line(start = [var -3, var -2], end = [var 3, var 2])
13470 midpoint(line1, point = ORIGIN)
13471}
13472"
13473 );
13474 assert_eq!(
13475 scene_delta.new_graph.objects.len(),
13476 6,
13477 "{:#?}",
13478 scene_delta.new_graph.objects
13479 );
13480
13481 ctx.close().await;
13482 }
13483
13484 #[tokio::test(flavor = "multi_thread")]
13485 async fn test_origin_arc_midpoint() {
13486 let initial_source = "\
13487sketch(on = XY) {
13488 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13489}
13490";
13491
13492 let program = Program::parse(initial_source).unwrap().0.unwrap();
13493
13494 let mut frontend = FrontendState::new();
13495
13496 let ctx = ExecutorContext::new_mock(None).await;
13497 let version = Version(0);
13498
13499 frontend.program = program.clone();
13500 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13501 frontend.update_state_after_exec(outcome, true);
13502 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13503 let sketch_id = sketch_object.id;
13504 let sketch = expect_sketch(sketch_object);
13505 let arc_id = *sketch.segments.get(3).unwrap();
13506
13507 let constraint = Constraint::Midpoint(Midpoint {
13508 point: ConstraintSegment::ORIGIN,
13509 segment: arc_id,
13510 });
13511 let (src_delta, scene_delta) = frontend
13512 .add_constraint(&ctx, version, sketch_id, constraint)
13513 .await
13514 .unwrap();
13515 assert_eq!(
13516 src_delta.text.as_str(),
13517 "\
13518sketch(on = XY) {
13519 arc1 = arc(start = [var 0.35, var 2.24], end = [var 1.62, var -1.58], center = [var 2.34, var 0.78])
13520 midpoint(arc1, point = ORIGIN)
13521}
13522"
13523 );
13524 assert_eq!(
13525 scene_delta.new_graph.objects.len(),
13526 7,
13527 "{:#?}",
13528 scene_delta.new_graph.objects
13529 );
13530
13531 ctx.close().await;
13532 }
13533
13534 #[tokio::test(flavor = "multi_thread")]
13535 async fn test_segments_symmetric_arcs() {
13536 let initial_source = "\
13537sketch(on = XY) {
13538 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
13539 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
13540 line(start = [var 0, var -10], end = [var 0, var 10])
13541}
13542";
13543
13544 let program = Program::parse(initial_source).unwrap().0.unwrap();
13545
13546 let mut frontend = FrontendState::new();
13547
13548 let ctx = ExecutorContext::new_mock(None).await;
13549 let version = Version(0);
13550
13551 frontend.program = program.clone();
13552 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13553 frontend.update_state_after_exec(outcome, true);
13554 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13555 let sketch_id = sketch_object.id;
13556 let sketch = expect_sketch(sketch_object);
13557 let arc1_id = *sketch.segments.get(3).unwrap();
13558 let arc2_id = *sketch.segments.get(7).unwrap();
13559 let axis_id = *sketch.segments.get(10).unwrap();
13560
13561 let constraint = Constraint::Symmetric(Symmetric {
13562 input: vec![arc1_id, arc2_id],
13563 axis: axis_id,
13564 });
13565 let (src_delta, scene_delta) = frontend
13566 .add_constraint(&ctx, version, sketch_id, constraint)
13567 .await
13568 .unwrap();
13569 assert_eq!(
13570 src_delta.text.as_str(),
13571 "\
13572sketch(on = XY) {
13573 arc1 = arc(start = [var -14.46, var 0], end = [var -10, var 4.65], center = [var -10.14, var 0.31])
13574 arc2 = arc(start = [var 5.49, var 2.26], end = [var 11.58, var -3.47], center = [var 9.34, var 0.25])
13575 line1 = line(start = [var -0.44, var -10], end = [var -0.37, var 10])
13576 symmetric([arc1, arc2], axis = line1)
13577}
13578"
13579 );
13580 assert_eq!(
13581 scene_delta.new_graph.objects.len(),
13582 14,
13583 "{:#?}",
13584 scene_delta.new_graph.objects
13585 );
13586
13587 ctx.close().await;
13588 }
13589
13590 #[tokio::test(flavor = "multi_thread")]
13591 async fn test_sketch_on_face_simple() {
13592 let initial_source = "\
13593len = 2mm
13594cube = startSketchOn(XY)
13595 |> startProfile(at = [0, 0])
13596 |> line(end = [len, 0], tag = $side)
13597 |> line(end = [0, len])
13598 |> line(end = [-len, 0])
13599 |> line(end = [0, -len])
13600 |> close()
13601 |> extrude(length = len)
13602
13603face = faceOf(cube, face = side)
13604";
13605
13606 let program = Program::parse(initial_source).unwrap().0.unwrap();
13607
13608 let mut frontend = FrontendState::new();
13609
13610 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13611 let mock_ctx = ExecutorContext::new_mock(None).await;
13612 let version = Version(0);
13613
13614 frontend.hack_set_program(&ctx, program).await.unwrap();
13615 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
13616 let face_id = face_object.id;
13617
13618 let sketch_args = SketchCtor {
13619 on: Plane::Object(face_id),
13620 };
13621 let (_src_delta, scene_delta, sketch_id) = frontend
13622 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13623 .await
13624 .unwrap();
13625 assert_eq!(sketch_id, ObjectId(2));
13626 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13627 let sketch_object = &scene_delta.new_graph.objects[2];
13628 assert_eq!(sketch_object.id, ObjectId(2));
13629 assert_eq!(
13630 sketch_object.kind,
13631 ObjectKind::Sketch(Sketch {
13632 args: SketchCtor {
13633 on: Plane::Object(face_id),
13634 },
13635 plane: face_id,
13636 segments: vec![],
13637 constraints: vec![],
13638 })
13639 );
13640 assert_eq!(scene_delta.new_graph.objects.len(), 8);
13641
13642 ctx.close().await;
13643 mock_ctx.close().await;
13644 }
13645
13646 #[tokio::test(flavor = "multi_thread")]
13647 async fn test_sketch_on_wall_artifact_from_region_extrude() {
13648 let initial_source = "\
13649s = sketch(on = YZ) {
13650 line1 = line(start = [0, 0], end = [0, 1])
13651 line2 = line(start = [0, 1], end = [1, 1])
13652 line3 = line(start = [1, 1], end = [0, 0])
13653}
13654region001 = region(point = [0.1, 0.1], sketch = s)
13655extrude001 = extrude(region001, length = 5)
13656";
13657
13658 let program = Program::parse(initial_source).unwrap().0.unwrap();
13659
13660 let mut frontend = FrontendState::new();
13661 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13662 let version = Version(0);
13663
13664 frontend.hack_set_program(&ctx, program).await.unwrap();
13665 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13666
13667 let sketch_args = SketchCtor {
13668 on: Plane::Object(wall_object_id),
13669 };
13670 let (src_delta, _scene_delta, _sketch_id) = frontend
13671 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13672 .await
13673 .unwrap();
13674 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13675
13676 ctx.close().await;
13677 }
13678
13679 #[tokio::test(flavor = "multi_thread")]
13680 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
13681 let initial_source = "\
13682sketch001 = sketch(on = YZ) {
13683 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
13684 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
13685 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
13686 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
13687 coincident([line1.end, line2.start])
13688 coincident([line2.end, line3.start])
13689 coincident([line3.end, line4.start])
13690 coincident([line4.end, line1.start])
13691 parallel([line2, line4])
13692 parallel([line3, line1])
13693 perpendicular([line1, line2])
13694 horizontal(line3)
13695 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
13696}
13697region001 = region(point = [3.1, 3.74], sketch = sketch001)
13698extrude001 = extrude(region001, length = 5)
13699";
13700
13701 let program = Program::parse(initial_source).unwrap().0.unwrap();
13702
13703 let mut frontend = FrontendState::new();
13704 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13705 let version = Version(0);
13706
13707 frontend.hack_set_program(&ctx, program).await.unwrap();
13708 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13709
13710 let sketch_args = SketchCtor {
13711 on: Plane::Object(wall_object_id),
13712 };
13713 let (src_delta, _scene_delta, _sketch_id) = frontend
13714 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13715 .await
13716 .unwrap();
13717 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13718
13719 ctx.close().await;
13720 }
13721
13722 #[tokio::test(flavor = "multi_thread")]
13723 async fn test_sketch_on_plane_incremental() {
13724 let initial_source = "\
13725len = 2mm
13726cube = startSketchOn(XY)
13727 |> startProfile(at = [0, 0])
13728 |> line(end = [len, 0], tag = $side)
13729 |> line(end = [0, len])
13730 |> line(end = [-len, 0])
13731 |> line(end = [0, -len])
13732 |> close()
13733 |> extrude(length = len)
13734
13735plane = planeOf(cube, face = side)
13736";
13737
13738 let program = Program::parse(initial_source).unwrap().0.unwrap();
13739
13740 let mut frontend = FrontendState::new();
13741
13742 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13743 let mock_ctx = ExecutorContext::new_mock(None).await;
13744 let version = Version(0);
13745
13746 frontend.hack_set_program(&ctx, program).await.unwrap();
13747 let plane_object = frontend
13749 .scene_graph
13750 .objects
13751 .iter()
13752 .rev()
13753 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
13754 .unwrap();
13755 let plane_id = plane_object.id;
13756
13757 let sketch_args = SketchCtor {
13758 on: Plane::Object(plane_id),
13759 };
13760 let (src_delta, scene_delta, sketch_id) = frontend
13761 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13762 .await
13763 .unwrap();
13764 assert_eq!(
13765 src_delta.text.as_str(),
13766 "\
13767len = 2mm
13768cube = startSketchOn(XY)
13769 |> startProfile(at = [0, 0])
13770 |> line(end = [len, 0], tag = $side)
13771 |> line(end = [0, len])
13772 |> line(end = [-len, 0])
13773 |> line(end = [0, -len])
13774 |> close()
13775 |> extrude(length = len)
13776
13777plane = planeOf(cube, face = side)
13778sketch001 = sketch(on = plane) {
13779}
13780"
13781 );
13782 assert_eq!(sketch_id, ObjectId(2));
13783 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13784 let sketch_object = &scene_delta.new_graph.objects[2];
13785 assert_eq!(sketch_object.id, ObjectId(2));
13786 assert_eq!(
13787 sketch_object.kind,
13788 ObjectKind::Sketch(Sketch {
13789 args: SketchCtor {
13790 on: Plane::Object(plane_id),
13791 },
13792 plane: plane_id,
13793 segments: vec![],
13794 constraints: vec![],
13795 })
13796 );
13797 assert_eq!(scene_delta.new_graph.objects.len(), 9);
13798
13799 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
13800 assert_eq!(plane_object.id, plane_id);
13801 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
13802
13803 ctx.close().await;
13804 mock_ctx.close().await;
13805 }
13806
13807 #[tokio::test(flavor = "multi_thread")]
13808 async fn test_new_sketch_uses_unique_variable_name() {
13809 let initial_source = "\
13810sketch1 = sketch(on = XY) {
13811}
13812";
13813
13814 let program = Program::parse(initial_source).unwrap().0.unwrap();
13815
13816 let mut frontend = FrontendState::new();
13817 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13818 let version = Version(0);
13819
13820 frontend.hack_set_program(&ctx, program).await.unwrap();
13821
13822 let sketch_args = SketchCtor {
13823 on: Plane::Default(PlaneName::Yz),
13824 };
13825 let (src_delta, _, _) = frontend
13826 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13827 .await
13828 .unwrap();
13829
13830 assert_eq!(
13831 src_delta.text.as_str(),
13832 "\
13833sketch1 = sketch(on = XY) {
13834}
13835sketch001 = sketch(on = YZ) {
13836}
13837"
13838 );
13839
13840 ctx.close().await;
13841 }
13842
13843 #[tokio::test(flavor = "multi_thread")]
13844 async fn test_new_sketch_twice_using_same_plane() {
13845 let initial_source = "\
13846sketch1 = sketch(on = XY) {
13847}
13848";
13849
13850 let program = Program::parse(initial_source).unwrap().0.unwrap();
13851
13852 let mut frontend = FrontendState::new();
13853 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13854 let version = Version(0);
13855
13856 frontend.hack_set_program(&ctx, program).await.unwrap();
13857
13858 let sketch_args = SketchCtor {
13859 on: Plane::Default(PlaneName::Xy),
13860 };
13861 let (src_delta, _, _) = frontend
13862 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13863 .await
13864 .unwrap();
13865
13866 assert_eq!(
13867 src_delta.text.as_str(),
13868 "\
13869sketch1 = sketch(on = XY) {
13870}
13871sketch001 = sketch(on = XY) {
13872}
13873"
13874 );
13875
13876 ctx.close().await;
13877 }
13878
13879 #[tokio::test(flavor = "multi_thread")]
13880 async fn test_sketch_mode_reuses_cached_on_expression() {
13881 let initial_source = "\
13882width = 2mm
13883sketch(on = offsetPlane(XY, offset = width)) {
13884 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
13885 distance([line1.start, line1.end]) == width
13886}
13887";
13888 let program = Program::parse(initial_source).unwrap().0.unwrap();
13889
13890 let mut frontend = FrontendState::new();
13891 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13892 let mock_ctx = ExecutorContext::new_mock(None).await;
13893 let version = Version(0);
13894 let project_id = ProjectId(0);
13895 let file_id = FileId(0);
13896
13897 frontend.hack_set_program(&ctx, program).await.unwrap();
13898 let initial_object_count = frontend.scene_graph.objects.len();
13899 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
13900 .expect("Expected sketch object to exist")
13901 .id;
13902
13903 let scene_delta = frontend
13906 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
13907 .await
13908 .unwrap();
13909 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
13910
13911 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
13914 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
13915
13916 ctx.close().await;
13917 mock_ctx.close().await;
13918 }
13919
13920 #[tokio::test(flavor = "multi_thread")]
13921 async fn test_multiple_sketch_blocks() {
13922 let initial_source = "\
13923// Cube that requires the engine.
13924width = 2
13925sketch001 = startSketchOn(XY)
13926profile001 = startProfile(sketch001, at = [0, 0])
13927 |> yLine(length = width, tag = $seg1)
13928 |> xLine(length = width)
13929 |> yLine(length = -width)
13930 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
13931 |> close()
13932extrude001 = extrude(profile001, length = width)
13933
13934// Get a value that requires the engine.
13935x = segLen(seg1)
13936
13937// Triangle with side length 2*x.
13938sketch(on = XY) {
13939 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
13940 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
13941 coincident([line1.end, line2.start])
13942 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
13943 coincident([line2.end, line3.start])
13944 coincident([line3.end, line1.start])
13945 equalLength([line3, line1])
13946 equalLength([line1, line2])
13947 distance([line1.start, line1.end]) == 2*x
13948}
13949
13950// Line segment with length x.
13951sketch2 = sketch(on = XY) {
13952 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
13953 distance([line1.start, line1.end]) == x
13954}
13955";
13956
13957 let program = Program::parse(initial_source).unwrap().0.unwrap();
13958
13959 let mut frontend = FrontendState::new();
13960
13961 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13962 let mock_ctx = ExecutorContext::new_mock(None).await;
13963 let version = Version(0);
13964 let project_id = ProjectId(0);
13965 let file_id = FileId(0);
13966
13967 frontend.hack_set_program(&ctx, program).await.unwrap();
13968 let sketch_objects = frontend
13969 .scene_graph
13970 .objects
13971 .iter()
13972 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
13973 .collect::<Vec<_>>();
13974 let sketch1_id = sketch_objects.first().unwrap().id;
13975 let sketch2_id = sketch_objects.get(1).unwrap().id;
13976 let point1_id = ObjectId(sketch1_id.0 + 1);
13978 let point2_id = ObjectId(sketch2_id.0 + 1);
13980
13981 let scene_delta = frontend
13990 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
13991 .await
13992 .unwrap();
13993 assert_eq!(
13994 scene_delta.new_graph.objects.len(),
13995 18,
13996 "{:#?}",
13997 scene_delta.new_graph.objects
13998 );
13999
14000 let point_ctor = PointCtor {
14002 position: Point2d {
14003 x: Expr::Var(Number {
14004 value: 1.0,
14005 units: NumericSuffix::Mm,
14006 }),
14007 y: Expr::Var(Number {
14008 value: 2.0,
14009 units: NumericSuffix::Mm,
14010 }),
14011 },
14012 };
14013 let segments = vec![ExistingSegmentCtor {
14014 id: point1_id,
14015 ctor: SegmentCtor::Point(point_ctor),
14016 }];
14017 let (src_delta, _) = frontend
14018 .edit_segments(&mock_ctx, version, sketch1_id, segments)
14019 .await
14020 .unwrap();
14021 assert_eq!(
14023 src_delta.text.as_str(),
14024 "\
14025// Cube that requires the engine.
14026width = 2
14027sketch001 = startSketchOn(XY)
14028profile001 = startProfile(sketch001, at = [0, 0])
14029 |> yLine(length = width, tag = $seg1)
14030 |> xLine(length = width)
14031 |> yLine(length = -width)
14032 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14033 |> close()
14034extrude001 = extrude(profile001, length = width)
14035
14036// Get a value that requires the engine.
14037x = segLen(seg1)
14038
14039// Triangle with side length 2*x.
14040sketch(on = XY) {
14041 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14042 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14043 coincident([line1.end, line2.start])
14044 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14045 coincident([line2.end, line3.start])
14046 coincident([line3.end, line1.start])
14047 equalLength([line3, line1])
14048 equalLength([line1, line2])
14049 distance([line1.start, line1.end]) == 2 * x
14050}
14051
14052// Line segment with length x.
14053sketch2 = sketch(on = XY) {
14054 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14055 distance([line1.start, line1.end]) == x
14056}
14057"
14058 );
14059 let edited_sketch1_source = src_delta.text.clone();
14060
14061 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
14063 assert_eq!(src_delta.text, edited_sketch1_source);
14064 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14072 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
14073
14074 let scene_delta = frontend
14082 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14083 .await
14084 .unwrap();
14085 assert_eq!(
14086 scene_delta.new_graph.objects.len(),
14087 24,
14088 "{:#?}",
14089 scene_delta.new_graph.objects
14090 );
14091
14092 let point_ctor = PointCtor {
14094 position: Point2d {
14095 x: Expr::Var(Number {
14096 value: 3.0,
14097 units: NumericSuffix::Mm,
14098 }),
14099 y: Expr::Var(Number {
14100 value: 4.0,
14101 units: NumericSuffix::Mm,
14102 }),
14103 },
14104 };
14105 let segments = vec![ExistingSegmentCtor {
14106 id: point2_id,
14107 ctor: SegmentCtor::Point(point_ctor),
14108 }];
14109 let (src_delta, _) = frontend
14110 .edit_segments(&mock_ctx, version, sketch2_id, segments)
14111 .await
14112 .unwrap();
14113 assert_eq!(
14115 src_delta.text.as_str(),
14116 "\
14117// Cube that requires the engine.
14118width = 2
14119sketch001 = startSketchOn(XY)
14120profile001 = startProfile(sketch001, at = [0, 0])
14121 |> yLine(length = width, tag = $seg1)
14122 |> xLine(length = width)
14123 |> yLine(length = -width)
14124 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14125 |> close()
14126extrude001 = extrude(profile001, length = width)
14127
14128// Get a value that requires the engine.
14129x = segLen(seg1)
14130
14131// Triangle with side length 2*x.
14132sketch(on = XY) {
14133 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14134 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14135 coincident([line1.end, line2.start])
14136 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14137 coincident([line2.end, line3.start])
14138 coincident([line3.end, line1.start])
14139 equalLength([line3, line1])
14140 equalLength([line1, line2])
14141 distance([line1.start, line1.end]) == 2 * x
14142}
14143
14144// Line segment with length x.
14145sketch2 = sketch(on = XY) {
14146 line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
14147 distance([line1.start, line1.end]) == x
14148}
14149"
14150 );
14151 let edited_sketch2_source = src_delta.text.clone();
14152
14153 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
14155 assert_eq!(src_delta.text, edited_sketch2_source);
14156
14157 ctx.close().await;
14158 mock_ctx.close().await;
14159 }
14160
14161 #[tokio::test(flavor = "multi_thread")]
14162 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
14163 clear_mem_cache().await;
14164
14165 let source = r#"sketch001 = sketch(on = XZ) {
14166 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
14167}
14168sketch002 = sketch(on = XY) {
14169 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
14170 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
14171 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
14172 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
14173 coincident([line1.end, line2.start])
14174 coincident([line2.end, line3.start])
14175 coincident([line3.end, line4.start])
14176 coincident([line4.end, line1.start])
14177 parallel([line2, line4])
14178 parallel([line3, line1])
14179 perpendicular([line1, line2])
14180 horizontal(line3)
14181 coincident([line1.start, ORIGIN])
14182}
14183"#;
14184
14185 let program = Program::parse(source).unwrap().0.unwrap();
14186 let mut frontend = FrontendState::new();
14187 let ctx = ExecutorContext::new_with_engine(
14188 std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
14189 Default::default(),
14190 );
14191 let mock_ctx = ExecutorContext::new_mock(None).await;
14192 let version = Version(0);
14193 let project_id = ProjectId(0);
14194 let file_id = FileId(0);
14195
14196 frontend.hack_set_program(&ctx, program).await.unwrap();
14197 let sketch_objects = frontend
14198 .scene_graph
14199 .objects
14200 .iter()
14201 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
14202 .collect::<Vec<_>>();
14203 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
14204
14205 let sketch1_id = sketch_objects[0].id;
14206 let sketch2_id = sketch_objects[1].id;
14207
14208 frontend
14209 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14210 .await
14211 .unwrap();
14212 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14213
14214 let scene_delta = frontend
14215 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14216 .await
14217 .unwrap();
14218 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
14219
14220 clear_mem_cache().await;
14221 ctx.close().await;
14222 mock_ctx.close().await;
14223 }
14224
14225 #[tokio::test(flavor = "multi_thread")]
14230 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
14231 let initial_source = "@settings(defaultLengthUnit = mm)
14233
14234
14235
14236sketch001 = sketch(on = XY) {
14237 point(at = [1in, 2in])
14238}
14239";
14240
14241 let program = Program::parse(initial_source).unwrap().0.unwrap();
14242 let mut frontend = FrontendState::new();
14243
14244 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14245 let mock_ctx = ExecutorContext::new_mock(None).await;
14246 let version = Version(0);
14247 let project_id = ProjectId(0);
14248 let file_id = FileId(0);
14249
14250 frontend.hack_set_program(&ctx, program).await.unwrap();
14251 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14252 let sketch_id = sketch_object.id;
14253
14254 frontend
14256 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14257 .await
14258 .unwrap();
14259
14260 let point_ctor = PointCtor {
14262 position: Point2d {
14263 x: Expr::Number(Number {
14264 value: 5.0,
14265 units: NumericSuffix::Mm,
14266 }),
14267 y: Expr::Number(Number {
14268 value: 6.0,
14269 units: NumericSuffix::Mm,
14270 }),
14271 },
14272 };
14273 let segment = SegmentCtor::Point(point_ctor);
14274 let (src_delta, scene_delta) = frontend
14275 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14276 .await
14277 .unwrap();
14278 assert!(
14280 src_delta.text.contains("point(at = [5mm, 6mm])"),
14281 "Expected new point in source, got: {}",
14282 src_delta.text
14283 );
14284 assert!(!scene_delta.new_objects.is_empty());
14285
14286 ctx.close().await;
14287 mock_ctx.close().await;
14288 }
14289
14290 #[tokio::test(flavor = "multi_thread")]
14291 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
14292 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
14293
14294 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
14295 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
14296
14297 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
14298 assert!(
14299 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
14300 "Expected experimental settings to be added to source"
14301 );
14302 }
14303
14304 #[tokio::test(flavor = "multi_thread")]
14305 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
14306 let initial_source = "@settings(defaultLengthUnit = mm)
14308
14309
14310
14311s = sketch(on = XY) {}
14312";
14313
14314 let program = Program::parse(initial_source).unwrap().0.unwrap();
14315 let mut frontend = FrontendState::new();
14316
14317 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14318 let mock_ctx = ExecutorContext::new_mock(None).await;
14319 let version = Version(0);
14320
14321 frontend.hack_set_program(&ctx, program).await.unwrap();
14322 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14323 let sketch_id = sketch_object.id;
14324
14325 let line_ctor = LineCtor {
14326 start: Point2d {
14327 x: Expr::Number(Number {
14328 value: 0.0,
14329 units: NumericSuffix::Mm,
14330 }),
14331 y: Expr::Number(Number {
14332 value: 0.0,
14333 units: NumericSuffix::Mm,
14334 }),
14335 },
14336 end: Point2d {
14337 x: Expr::Number(Number {
14338 value: 10.0,
14339 units: NumericSuffix::Mm,
14340 }),
14341 y: Expr::Number(Number {
14342 value: 10.0,
14343 units: NumericSuffix::Mm,
14344 }),
14345 },
14346 construction: None,
14347 };
14348 let segment = SegmentCtor::Line(line_ctor);
14349 let (src_delta, scene_delta) = frontend
14350 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14351 .await
14352 .unwrap();
14353 assert!(
14354 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14355 "Expected line in source, got: {}",
14356 src_delta.text
14357 );
14358 assert_eq!(scene_delta.new_objects.len(), 3);
14360
14361 ctx.close().await;
14362 mock_ctx.close().await;
14363 }
14364
14365 #[tokio::test(flavor = "multi_thread")]
14366 async fn test_extra_newlines_between_operations_edit_line() {
14367 let initial_source = "@settings(defaultLengthUnit = mm)
14369
14370
14371sketch001 = sketch(on = XY) {
14372
14373 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14374
14375}
14376";
14377
14378 let program = Program::parse(initial_source).unwrap().0.unwrap();
14379 let mut frontend = FrontendState::new();
14380
14381 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14382 let mock_ctx = ExecutorContext::new_mock(None).await;
14383 let version = Version(0);
14384 let project_id = ProjectId(0);
14385 let file_id = FileId(0);
14386
14387 frontend.hack_set_program(&ctx, program).await.unwrap();
14388 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14389 let sketch_id = sketch_object.id;
14390 let sketch = expect_sketch(sketch_object);
14391
14392 let line_id = sketch
14394 .segments
14395 .iter()
14396 .copied()
14397 .find(|seg_id| {
14398 matches!(
14399 &frontend.scene_graph.objects[seg_id.0].kind,
14400 ObjectKind::Segment {
14401 segment: Segment::Line(_)
14402 }
14403 )
14404 })
14405 .expect("Expected a line segment in sketch");
14406
14407 frontend
14409 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14410 .await
14411 .unwrap();
14412
14413 let line_ctor = LineCtor {
14415 start: Point2d {
14416 x: Expr::Var(Number {
14417 value: 1.0,
14418 units: NumericSuffix::Mm,
14419 }),
14420 y: Expr::Var(Number {
14421 value: 2.0,
14422 units: NumericSuffix::Mm,
14423 }),
14424 },
14425 end: Point2d {
14426 x: Expr::Var(Number {
14427 value: 13.0,
14428 units: NumericSuffix::Mm,
14429 }),
14430 y: Expr::Var(Number {
14431 value: 14.0,
14432 units: NumericSuffix::Mm,
14433 }),
14434 },
14435 construction: None,
14436 };
14437 let segments = vec![ExistingSegmentCtor {
14438 id: line_id,
14439 ctor: SegmentCtor::Line(line_ctor),
14440 }];
14441 let (src_delta, _scene_delta) = frontend
14442 .edit_segments(&mock_ctx, version, sketch_id, segments)
14443 .await
14444 .unwrap();
14445 assert!(
14446 src_delta
14447 .text
14448 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
14449 "Expected edited line in source, got: {}",
14450 src_delta.text
14451 );
14452
14453 ctx.close().await;
14454 mock_ctx.close().await;
14455 }
14456
14457 #[tokio::test(flavor = "multi_thread")]
14458 async fn test_extra_newlines_delete_segment() {
14459 let initial_source = "@settings(defaultLengthUnit = mm)
14461
14462
14463
14464sketch001 = sketch(on = XY) {
14465 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14466}
14467";
14468
14469 let program = Program::parse(initial_source).unwrap().0.unwrap();
14470 let mut frontend = FrontendState::new();
14471
14472 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14473 let mock_ctx = ExecutorContext::new_mock(None).await;
14474 let version = Version(0);
14475
14476 frontend.hack_set_program(&ctx, program).await.unwrap();
14477 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14478 let sketch_id = sketch_object.id;
14479 let sketch = expect_sketch(sketch_object);
14480
14481 assert_eq!(sketch.segments.len(), 3);
14483 let circle_id = sketch.segments[2];
14484
14485 let (src_delta, scene_delta) = frontend
14487 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
14488 .await
14489 .unwrap();
14490 assert!(
14491 src_delta.text.contains("sketch(on = XY) {"),
14492 "Expected sketch block in source, got: {}",
14493 src_delta.text
14494 );
14495 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
14496 let new_sketch = expect_sketch(new_sketch_object);
14497 assert_eq!(new_sketch.segments.len(), 0);
14498
14499 ctx.close().await;
14500 mock_ctx.close().await;
14501 }
14502
14503 #[tokio::test(flavor = "multi_thread")]
14504 async fn test_unformatted_source_add_arc() {
14505 let initial_source = "@settings(defaultLengthUnit = mm)
14507
14508
14509
14510
14511sketch001 = sketch(on = XY) {
14512}
14513";
14514
14515 let program = Program::parse(initial_source).unwrap().0.unwrap();
14516 let mut frontend = FrontendState::new();
14517
14518 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14519 let mock_ctx = ExecutorContext::new_mock(None).await;
14520 let version = Version(0);
14521
14522 frontend.hack_set_program(&ctx, program).await.unwrap();
14523 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14524 let sketch_id = sketch_object.id;
14525
14526 let arc_ctor = ArcCtor {
14527 start: Point2d {
14528 x: Expr::Var(Number {
14529 value: 5.0,
14530 units: NumericSuffix::Mm,
14531 }),
14532 y: Expr::Var(Number {
14533 value: 0.0,
14534 units: NumericSuffix::Mm,
14535 }),
14536 },
14537 end: Point2d {
14538 x: Expr::Var(Number {
14539 value: 0.0,
14540 units: NumericSuffix::Mm,
14541 }),
14542 y: Expr::Var(Number {
14543 value: 5.0,
14544 units: NumericSuffix::Mm,
14545 }),
14546 },
14547 center: Point2d {
14548 x: Expr::Var(Number {
14549 value: 0.0,
14550 units: NumericSuffix::Mm,
14551 }),
14552 y: Expr::Var(Number {
14553 value: 0.0,
14554 units: NumericSuffix::Mm,
14555 }),
14556 },
14557 construction: None,
14558 };
14559 let segment = SegmentCtor::Arc(arc_ctor);
14560 let (src_delta, scene_delta) = frontend
14561 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14562 .await
14563 .unwrap();
14564 assert!(
14565 src_delta
14566 .text
14567 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
14568 "Expected arc in source, got: {}",
14569 src_delta.text
14570 );
14571 assert!(!scene_delta.new_objects.is_empty());
14572
14573 ctx.close().await;
14574 mock_ctx.close().await;
14575 }
14576
14577 #[tokio::test(flavor = "multi_thread")]
14578 async fn test_extra_newlines_add_circle() {
14579 let initial_source = "@settings(defaultLengthUnit = mm)
14581
14582
14583
14584sketch001 = sketch(on = XY) {
14585}
14586";
14587
14588 let program = Program::parse(initial_source).unwrap().0.unwrap();
14589 let mut frontend = FrontendState::new();
14590
14591 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14592 let mock_ctx = ExecutorContext::new_mock(None).await;
14593 let version = Version(0);
14594
14595 frontend.hack_set_program(&ctx, program).await.unwrap();
14596 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14597 let sketch_id = sketch_object.id;
14598
14599 let circle_ctor = CircleCtor {
14600 start: Point2d {
14601 x: Expr::Var(Number {
14602 value: 5.0,
14603 units: NumericSuffix::Mm,
14604 }),
14605 y: Expr::Var(Number {
14606 value: 0.0,
14607 units: NumericSuffix::Mm,
14608 }),
14609 },
14610 center: Point2d {
14611 x: Expr::Var(Number {
14612 value: 0.0,
14613 units: NumericSuffix::Mm,
14614 }),
14615 y: Expr::Var(Number {
14616 value: 0.0,
14617 units: NumericSuffix::Mm,
14618 }),
14619 },
14620 construction: None,
14621 };
14622 let segment = SegmentCtor::Circle(circle_ctor);
14623 let (src_delta, scene_delta) = frontend
14624 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14625 .await
14626 .unwrap();
14627 assert!(
14628 src_delta
14629 .text
14630 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
14631 "Expected circle in source, got: {}",
14632 src_delta.text
14633 );
14634 assert!(!scene_delta.new_objects.is_empty());
14635
14636 ctx.close().await;
14637 mock_ctx.close().await;
14638 }
14639
14640 #[tokio::test(flavor = "multi_thread")]
14641 async fn test_extra_newlines_add_constraint() {
14642 let initial_source = "@settings(defaultLengthUnit = mm)
14644
14645
14646
14647sketch001 = sketch(on = XY) {
14648 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14649 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
14650}
14651";
14652
14653 let program = Program::parse(initial_source).unwrap().0.unwrap();
14654 let mut frontend = FrontendState::new();
14655
14656 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14657 let mock_ctx = ExecutorContext::new_mock(None).await;
14658 let version = Version(0);
14659 let project_id = ProjectId(0);
14660 let file_id = FileId(0);
14661
14662 frontend.hack_set_program(&ctx, program).await.unwrap();
14663 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14664 let sketch_id = sketch_object.id;
14665 let sketch = expect_sketch(sketch_object);
14666
14667 let line_ids: Vec<ObjectId> = sketch
14669 .segments
14670 .iter()
14671 .copied()
14672 .filter(|seg_id| {
14673 matches!(
14674 &frontend.scene_graph.objects[seg_id.0].kind,
14675 ObjectKind::Segment {
14676 segment: Segment::Line(_)
14677 }
14678 )
14679 })
14680 .collect();
14681 assert_eq!(line_ids.len(), 2, "Expected two line segments");
14682
14683 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
14684 let ObjectKind::Segment {
14685 segment: Segment::Line(line1_data),
14686 } = &line1.kind
14687 else {
14688 panic!("Expected line");
14689 };
14690 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
14691 let ObjectKind::Segment {
14692 segment: Segment::Line(line2_data),
14693 } = &line2.kind
14694 else {
14695 panic!("Expected line");
14696 };
14697
14698 let constraint = Constraint::Coincident(Coincident {
14700 segments: vec![line1_data.end.into(), line2_data.start.into()],
14701 });
14702
14703 frontend
14705 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14706 .await
14707 .unwrap();
14708 let (src_delta, _scene_delta) = frontend
14709 .add_constraint(&mock_ctx, version, sketch_id, constraint)
14710 .await
14711 .unwrap();
14712 assert!(
14713 src_delta.text.contains("coincident("),
14714 "Expected coincident constraint in source, got: {}",
14715 src_delta.text
14716 );
14717
14718 ctx.close().await;
14719 mock_ctx.close().await;
14720 }
14721
14722 #[tokio::test(flavor = "multi_thread")]
14723 async fn test_extra_newlines_add_line_then_edit_line() {
14724 let initial_source = "@settings(defaultLengthUnit = mm)
14726
14727
14728
14729sketch001 = sketch(on = XY) {
14730}
14731";
14732
14733 let program = Program::parse(initial_source).unwrap().0.unwrap();
14734 let mut frontend = FrontendState::new();
14735
14736 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14737 let mock_ctx = ExecutorContext::new_mock(None).await;
14738 let version = Version(0);
14739
14740 frontend.hack_set_program(&ctx, program).await.unwrap();
14741 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14742 let sketch_id = sketch_object.id;
14743
14744 let line_ctor = LineCtor {
14746 start: Point2d {
14747 x: Expr::Number(Number {
14748 value: 0.0,
14749 units: NumericSuffix::Mm,
14750 }),
14751 y: Expr::Number(Number {
14752 value: 0.0,
14753 units: NumericSuffix::Mm,
14754 }),
14755 },
14756 end: Point2d {
14757 x: Expr::Number(Number {
14758 value: 10.0,
14759 units: NumericSuffix::Mm,
14760 }),
14761 y: Expr::Number(Number {
14762 value: 10.0,
14763 units: NumericSuffix::Mm,
14764 }),
14765 },
14766 construction: None,
14767 };
14768 let segment = SegmentCtor::Line(line_ctor);
14769 let (src_delta, scene_delta) = frontend
14770 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14771 .await
14772 .unwrap();
14773 assert!(
14774 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14775 "Expected line in source after add, got: {}",
14776 src_delta.text
14777 );
14778 let line_id = *scene_delta.new_objects.last().unwrap();
14780
14781 let line_ctor = LineCtor {
14783 start: Point2d {
14784 x: Expr::Number(Number {
14785 value: 1.0,
14786 units: NumericSuffix::Mm,
14787 }),
14788 y: Expr::Number(Number {
14789 value: 2.0,
14790 units: NumericSuffix::Mm,
14791 }),
14792 },
14793 end: Point2d {
14794 x: Expr::Number(Number {
14795 value: 13.0,
14796 units: NumericSuffix::Mm,
14797 }),
14798 y: Expr::Number(Number {
14799 value: 14.0,
14800 units: NumericSuffix::Mm,
14801 }),
14802 },
14803 construction: None,
14804 };
14805 let segments = vec![ExistingSegmentCtor {
14806 id: line_id,
14807 ctor: SegmentCtor::Line(line_ctor),
14808 }];
14809 let (src_delta, scene_delta) = frontend
14810 .edit_segments(&mock_ctx, version, sketch_id, segments)
14811 .await
14812 .unwrap();
14813 assert!(
14814 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
14815 "Expected edited line in source, got: {}",
14816 src_delta.text
14817 );
14818 assert_eq!(scene_delta.new_objects, vec![]);
14819
14820 ctx.close().await;
14821 mock_ctx.close().await;
14822 }
14823}