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::ArtifactId;
23use crate::execution::CapSubType;
24use crate::execution::CodeRef;
25use crate::execution::MockConfig;
26use crate::execution::SKETCH_BLOCK_PARAM_ON;
27use crate::execution::annotations::WarningLevel;
28use crate::execution::cache::SketchModeState;
29use crate::execution::cache::clear_mem_cache;
30use crate::execution::cache::read_old_memory;
31use crate::execution::cache::write_old_memory;
32use crate::execution::types::adjust_length;
33use crate::fmt::format_number_literal;
34use crate::front::Angle;
35use crate::front::ArcCtor;
36use crate::front::CircleCtor;
37use crate::front::ControlPointSplineCtor;
38use crate::front::Distance;
39use crate::front::EqualRadius;
40use crate::front::Error;
41use crate::front::ExecResult;
42use crate::front::FixedPoint;
43use crate::front::Freedom;
44use crate::front::LinesEqualLength;
45use crate::front::Midpoint;
46use crate::front::Object;
47use crate::front::Parallel;
48use crate::front::Perpendicular;
49use crate::front::PointCtor;
50use crate::front::Symmetric;
51use crate::front::Tangent;
52use crate::frontend::api::Expr;
53use crate::frontend::api::FileId;
54use crate::frontend::api::Number;
55use crate::frontend::api::ObjectId;
56use crate::frontend::api::ObjectKind;
57use crate::frontend::api::Plane;
58use crate::frontend::api::ProjectId;
59use crate::frontend::api::RestoreSketchCheckpointOutcome;
60use crate::frontend::api::SceneGraph;
61use crate::frontend::api::SceneGraphDelta;
62use crate::frontend::api::SketchCheckpointId;
63use crate::frontend::api::SourceDelta;
64use crate::frontend::api::SourceRef;
65use crate::frontend::api::Version;
66use crate::frontend::modify::find_defined_names;
67use crate::frontend::modify::next_free_name;
68use crate::frontend::modify::next_free_name_with_padding;
69use crate::frontend::sketch::Coincident;
70use crate::frontend::sketch::Constraint;
71use crate::frontend::sketch::ConstraintSegment;
72use crate::frontend::sketch::Diameter;
73use crate::frontend::sketch::ExistingSegmentCtor;
74use crate::frontend::sketch::Horizontal;
75use crate::frontend::sketch::LineCtor;
76use crate::frontend::sketch::Point2d;
77use crate::frontend::sketch::Radius;
78use crate::frontend::sketch::Segment;
79use crate::frontend::sketch::SegmentCtor;
80use crate::frontend::sketch::SketchApi;
81use crate::frontend::sketch::SketchCtor;
82use crate::frontend::sketch::Vertical;
83use crate::frontend::traverse::MutateBodyItem;
84use crate::frontend::traverse::TraversalReturn;
85use crate::frontend::traverse::Visitor;
86use crate::frontend::traverse::dfs_mut;
87use crate::id::IncIdGenerator;
88use crate::parsing::ast::types as ast;
89use crate::pretty::NumericSuffix;
90use crate::std::constraints::LinesAtAngleKind;
91use crate::walk::NodeMut;
92use crate::walk::Visitable;
93
94pub(crate) mod api;
95pub(crate) mod modify;
96pub(crate) mod sketch;
97
98pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
99
100#[derive(Debug, Clone)]
101struct SketchCheckpoint {
102 id: SketchCheckpointId,
103 source: SourceDelta,
104 program: Program,
105 scene_graph: SceneGraph,
106 exec_outcome: ExecOutcome,
107 point_freedom_cache: HashMap<ObjectId, Freedom>,
108 mock_memory: Option<SketchModeState>,
109}
110mod traverse;
111pub(crate) mod trim;
112
113struct ArcSizeConstraintParams {
114 points: Vec<ObjectId>,
115 function_name: &'static str,
116 value: f64,
117 units: NumericSuffix,
118 label_position: Option<Point2d<Number>>,
119 constraint_type_name: &'static str,
120}
121
122const POINT_FN: &str = "point";
123const POINT_AT_PARAM: &str = "at";
124const LINE_FN: &str = "line";
125const LINE_VARIABLE: &str = "line";
126const LINE_START_PARAM: &str = "start";
127const LINE_END_PARAM: &str = "end";
128const ARC_FN: &str = "arc";
129const ARC_VARIABLE: &str = "arc";
130const ARC_START_PARAM: &str = "start";
131const ARC_END_PARAM: &str = "end";
132const ARC_CENTER_PARAM: &str = "center";
133const CIRCLE_FN: &str = "circle";
134const CIRCLE_VARIABLE: &str = "circle";
135const CIRCLE_START_PARAM: &str = "start";
136const CIRCLE_CENTER_PARAM: &str = "center";
137const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
138const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
139const LABEL_POSITION_PARAM: &str = "labelPosition";
140
141const COINCIDENT_FN: &str = "coincident";
142const DIAMETER_FN: &str = "diameter";
143const DISTANCE_FN: &str = "distance";
144const FIXED_FN: &str = "fixed";
145const ANGLE_FN: &str = "angle";
146const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
147const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
148const EQUAL_LENGTH_FN: &str = "equalLength";
149const EQUAL_RADIUS_FN: &str = "equalRadius";
150const HORIZONTAL_FN: &str = "horizontal";
151const MIDPOINT_FN: &str = "midpoint";
152const MIDPOINT_POINT_PARAM: &str = "point";
153const RADIUS_FN: &str = "radius";
154const SYMMETRIC_FN: &str = "symmetric";
155const SYMMETRIC_AXIS_PARAM: &str = "axis";
156const TANGENT_FN: &str = "tangent";
157const VERTICAL_FN: &str = "vertical";
158
159const LINE_PROPERTY_START: &str = "start";
160const LINE_PROPERTY_END: &str = "end";
161
162const ARC_PROPERTY_START: &str = "start";
163const ARC_PROPERTY_END: &str = "end";
164const ARC_PROPERTY_CENTER: &str = "center";
165const CIRCLE_PROPERTY_START: &str = "start";
166const CIRCLE_PROPERTY_CENTER: &str = "center";
167const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
168const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
169
170const CONSTRUCTION_PARAM: &str = "construction";
171
172#[derive(Debug, Clone, Copy)]
173enum EditDeleteKind {
174 Edit,
175 DeleteNonSketch,
176}
177
178struct ExecuteAfterEditOptions {
180 segment_ids_edited: AhashIndexSet<ObjectId>,
181 edit_kind: EditDeleteKind,
182 commit_solved_initial_guesses: bool,
183}
184
185impl EditDeleteKind {
186 fn is_delete(&self) -> bool {
188 match self {
189 EditDeleteKind::Edit => false,
190 EditDeleteKind::DeleteNonSketch => true,
191 }
192 }
193
194 fn to_change_kind(self) -> ChangeKind {
195 match self {
196 EditDeleteKind::Edit => ChangeKind::Edit,
197 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
198 }
199 }
200}
201
202#[derive(Debug, Clone, Copy)]
203enum ChangeKind {
204 Add,
205 Edit,
206 Delete,
207 None,
208}
209
210#[derive(Debug, Clone, Serialize, ts_rs::TS)]
211#[ts(export, export_to = "FrontendApi.ts")]
212#[serde(tag = "type")]
213pub enum SetProgramOutcome {
214 #[serde(rename_all = "camelCase")]
215 Success {
216 scene_graph: Box<SceneGraph>,
217 exec_outcome: Box<ExecOutcome>,
218 checkpoint_id: Option<SketchCheckpointId>,
219 },
220 #[serde(rename_all = "camelCase")]
221 ExecFailure { error: Box<KclErrorWithOutputs> },
222}
223
224pub struct EditSegmentsOptions {
226 pub anchor_segment_ids: Option<Vec<ObjectId>>,
232 pub drag_anchors: Vec<SegmentDragAnchor>,
235 pub commit_solved_initial_guesses: bool,
237}
238
239pub struct EditDistanceConstraintLabelPositionOptions {
241 pub anchor_segment_ids: Vec<ObjectId>,
243 pub commit_solved_initial_guesses: bool,
245}
246
247#[derive(Debug, Clone)]
248pub struct FrontendState {
249 program: Program,
250 scene_graph: SceneGraph,
251 point_freedom_cache: HashMap<ObjectId, Freedom>,
254 next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
257 next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
260 next_edit_commits_solver_solutions: Option<bool>,
264 sketch_checkpoints: VecDeque<SketchCheckpoint>,
265 sketch_checkpoint_id_gen: IncIdGenerator<u64>,
266}
267
268impl Default for FrontendState {
269 fn default() -> Self {
270 Self::new()
271 }
272}
273
274impl FrontendState {
275 pub fn new() -> Self {
276 Self {
277 program: Program::empty(),
278 scene_graph: SceneGraph {
279 project: ProjectId(0),
280 file: FileId(0),
281 version: Version(0),
282 objects: Default::default(),
283 settings: Default::default(),
284 sketch_mode: Default::default(),
285 },
286 point_freedom_cache: HashMap::new(),
287 next_drag_anchor_segment_ids: None,
288 next_segment_drag_anchors: None,
289 next_edit_commits_solver_solutions: None,
290 sketch_checkpoints: VecDeque::new(),
291 sketch_checkpoint_id_gen: IncIdGenerator::new(1),
292 }
293 }
294
295 pub fn scene_graph(&self) -> &SceneGraph {
297 &self.scene_graph
298 }
299
300 pub fn default_length_unit(&self) -> UnitLength {
301 self.program
302 .meta_settings()
303 .ok()
304 .flatten()
305 .map(|settings| settings.default_length_units)
306 .unwrap_or(UnitLength::Millimeters)
307 }
308
309 pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
310 let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
311
312 let checkpoint = SketchCheckpoint {
313 id: checkpoint_id,
314 source: SourceDelta {
315 text: source_from_ast(&self.program.ast),
316 },
317 program: self.program.clone(),
318 scene_graph: self.scene_graph.clone(),
319 exec_outcome,
320 point_freedom_cache: self.point_freedom_cache.clone(),
321 mock_memory: read_old_memory().await,
322 };
323
324 self.sketch_checkpoints.push_back(checkpoint);
325 while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
326 self.sketch_checkpoints.pop_front();
327 }
328
329 Ok(checkpoint_id)
330 }
331
332 pub async fn edit_segments_with_options(
340 &mut self,
341 ctx: &ExecutorContext,
342 version: Version,
343 sketch: ObjectId,
344 segments: Vec<ExistingSegmentCtor>,
345 options: EditSegmentsOptions,
346 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
347 let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
348 self.next_drag_anchor_segment_ids
349 .replace(anchor_ids.into_iter().collect())
350 });
351 let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
352 let previous_commit_mode = self
353 .next_edit_commits_solver_solutions
354 .replace(options.commit_solved_initial_guesses);
355 let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
356 if let Some(previous_anchor_ids) = previous_anchor_ids {
357 self.next_drag_anchor_segment_ids = previous_anchor_ids;
358 }
359 self.next_segment_drag_anchors = previous_drag_anchors;
360 self.next_edit_commits_solver_solutions = previous_commit_mode;
361 result
362 }
363
364 pub async fn edit_distance_constraint_label_position_with_options(
370 &mut self,
371 ctx: &ExecutorContext,
372 version: Version,
373 sketch: ObjectId,
374 constraint_id: ObjectId,
375 label_position: Point2d<Number>,
376 options: EditDistanceConstraintLabelPositionOptions,
377 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
378 let previous_commit_mode = self
379 .next_edit_commits_solver_solutions
380 .replace(options.commit_solved_initial_guesses);
381 let result = SketchApi::edit_distance_constraint_label_position(
382 self,
383 ctx,
384 version,
385 sketch,
386 constraint_id,
387 label_position,
388 options.anchor_segment_ids,
389 )
390 .await;
391 self.next_edit_commits_solver_solutions = previous_commit_mode;
392 result
393 }
394
395 pub async fn restore_sketch_checkpoint(
396 &mut self,
397 checkpoint_id: SketchCheckpointId,
398 ) -> api::Result<RestoreSketchCheckpointOutcome> {
399 let checkpoint = self
400 .sketch_checkpoints
401 .iter()
402 .find(|checkpoint| checkpoint.id == checkpoint_id)
403 .cloned()
404 .ok_or_else(|| Error {
405 msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
406 })?;
407
408 self.program = checkpoint.program;
409 self.scene_graph = checkpoint.scene_graph.clone();
410 self.point_freedom_cache = checkpoint.point_freedom_cache;
411 self.next_drag_anchor_segment_ids = None;
412 self.next_segment_drag_anchors = None;
413 self.next_edit_commits_solver_solutions = None;
414
415 if let Some(mock_memory) = checkpoint.mock_memory {
416 write_old_memory(mock_memory).await;
417 } else {
418 clear_mem_cache().await;
419 }
420
421 Ok(RestoreSketchCheckpointOutcome {
422 source_delta: checkpoint.source,
423 scene_graph_delta: SceneGraphDelta {
424 new_graph: self.scene_graph_for_ui(),
425 new_objects: Vec::new(),
426 invalidates_ids: true,
427 exec_outcome: checkpoint.exec_outcome,
428 },
429 })
430 }
431
432 pub fn clear_sketch_checkpoints(&mut self) {
433 self.sketch_checkpoints.clear();
434 }
435 fn scene_graph_for_ui(&self) -> SceneGraph {
436 let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
437 matches!(
438 object.kind,
439 ObjectKind::Segment {
440 segment: Segment::ControlPointSpline(_)
441 }
442 )
443 });
444
445 if !has_control_point_splines {
446 return self.scene_graph.clone();
447 }
448
449 let hidden_constraint_ids = self
450 .scene_graph
451 .objects
452 .iter()
453 .filter_map(|object| match &object.kind {
454 ObjectKind::Constraint {
455 constraint: Constraint::Coincident(coincident),
456 } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
457 Some(object.id)
458 }
459 _ => None,
460 })
461 .collect::<HashSet<_>>();
462
463 if hidden_constraint_ids.is_empty() {
464 return self.scene_graph.clone();
465 }
466
467 let mut scene_graph = self.scene_graph.clone();
468 for object in &mut scene_graph.objects {
469 match &mut object.kind {
470 ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
471 object.kind = ObjectKind::Nil;
472 }
473 ObjectKind::Sketch(sketch) => {
474 sketch
475 .constraints
476 .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
477 }
478 _ => {}
479 }
480 }
481
482 scene_graph
483 }
484}
485
486fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
487 let mut first_owner_id = None;
488 for segment_id in coincident.segment_ids() {
489 let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
490 return false;
491 };
492
493 match first_owner_id {
494 Some(first_owner_id) if first_owner_id != owner_id => return false,
495 Some(_) => {}
496 None => first_owner_id = Some(owner_id),
497 }
498 }
499
500 first_owner_id.is_some()
501}
502
503fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
504 let object = scene_graph.objects.get(segment_id.0)?;
505 let ObjectKind::Segment { segment } = &object.kind else {
506 return None;
507 };
508
509 match segment {
510 Segment::ControlPointSpline(_) => Some(segment_id),
511 Segment::Point(point) => point
512 .owner
513 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
514 Segment::Line(line) => line
515 .owner
516 .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
517 _ => None,
518 }
519}
520
521fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
522 matches!(
523 scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
524 Some(ObjectKind::Segment {
525 segment: Segment::ControlPointSpline(_)
526 })
527 )
528}
529
530fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
531 let experimental_features_allowed = program
532 .meta_settings()
533 .ok()
534 .flatten()
535 .map(|settings| settings.experimental_features == WarningLevel::Allow)
536 .unwrap_or(false);
537 if experimental_features_allowed {
538 return Ok(program.clone());
539 }
540
541 program.change_experimental_features(Some(WarningLevel::Allow))
542}
543
544impl SketchApi for FrontendState {
545 async fn execute_mock(
546 &mut self,
547 ctx: &ExecutorContext,
548 _version: Version,
549 sketch: ObjectId,
550 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
551 let sketch_block_ref =
552 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
553
554 let mut truncated_program = self.program.clone();
555 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
556 .map_err(KclErrorWithOutputs::no_outputs)?;
557
558 let outcome = ctx
560 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
561 .await?;
562 let new_source = source_from_ast(&self.program.ast);
563 let src_delta = SourceDelta { text: new_source };
564 let outcome = self.update_state_after_exec(outcome, true);
566 let scene_graph_delta = SceneGraphDelta {
567 new_graph: self.scene_graph.clone(),
568 new_objects: Default::default(),
569 invalidates_ids: false,
570 exec_outcome: outcome,
571 };
572 Ok((src_delta, scene_graph_delta))
573 }
574
575 async fn new_sketch(
576 &mut self,
577 ctx: &ExecutorContext,
578 _project: ProjectId,
579 _file: FileId,
580 _version: Version,
581 args: SketchCtor,
582 ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
583 let mut new_ast = self.program.ast.clone();
586 let mut plane_ast =
588 sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &args.on).map_err(KclErrorWithOutputs::no_outputs)?;
589 let mut defined_names = find_defined_names(&new_ast);
590 let is_face_of_expr = matches!(
591 &plane_ast,
592 ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
593 );
594 if is_face_of_expr {
595 let face_name = next_free_name_with_padding("face", &defined_names)
596 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
597 let face_decl = ast::VariableDeclaration::new(
598 ast::VariableDeclarator::new(&face_name, plane_ast),
599 ast::ItemVisibility::Default,
600 ast::VariableKind::Const,
601 );
602 new_ast
603 .body
604 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
605 face_decl,
606 ))));
607 defined_names.insert(face_name.clone());
608 plane_ast = ast::Expr::Name(Box::new(ast::Name::new(&face_name)));
609 }
610 let sketch_ast = ast::SketchBlock {
611 arguments: vec![ast::LabeledArg {
612 label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
613 arg: plane_ast,
614 }],
615 body: Default::default(),
616 is_being_edited: false,
617 non_code_meta: Default::default(),
618 digest: None,
619 };
620 let sketch_name = next_free_name_with_padding("sketch", &defined_names)
623 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
624 let sketch_decl = ast::VariableDeclaration::new(
625 ast::VariableDeclarator::new(
626 &sketch_name,
627 ast::Expr::SketchBlock(Box::new(ast::Node::no_src(sketch_ast))),
628 ),
629 ast::ItemVisibility::Default,
630 ast::VariableKind::Const,
631 );
632 new_ast
633 .body
634 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
635 sketch_decl,
636 ))));
637 let new_source = source_from_ast(&new_ast);
639 let (new_program, errors) = Program::parse(&new_source)
641 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
642 if !errors.is_empty() {
643 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
644 "Error parsing KCL source after adding sketch: {errors:?}"
645 ))));
646 }
647 let Some(new_program) = new_program else {
648 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
649 "No AST produced after adding sketch".to_owned(),
650 )));
651 };
652
653 self.program = new_program.clone();
655
656 let outcome = ctx.run_with_caching(new_program.clone()).await?;
659 let freedom_analysis_ran = true;
660
661 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
662
663 let Some(sketch_id) = self
664 .scene_graph
665 .objects
666 .iter()
667 .filter_map(|object| match object.kind {
668 ObjectKind::Sketch(_) => Some(object.id),
669 _ => None,
670 })
671 .max_by_key(|id| id.0)
672 else {
673 return Err(KclErrorWithOutputs::from_error_outcome(
674 KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
675 outcome,
676 ));
677 };
678 self.scene_graph.sketch_mode = Some(sketch_id);
680
681 let src_delta = SourceDelta { text: new_source };
682 let scene_graph_delta = SceneGraphDelta {
683 new_graph: self.scene_graph_for_ui(),
684 invalidates_ids: false,
685 new_objects: vec![sketch_id],
686 exec_outcome: outcome,
687 };
688 Ok((src_delta, scene_graph_delta, sketch_id))
689 }
690
691 async fn edit_sketch(
692 &mut self,
693 ctx: &ExecutorContext,
694 _project: ProjectId,
695 _file: FileId,
696 _version: Version,
697 sketch: ObjectId,
698 ) -> ExecResult<SceneGraphDelta> {
699 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
703 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
704 })?;
705 let ObjectKind::Sketch(_) = &sketch_object.kind else {
706 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
707 "Object is not a sketch, it is {}",
708 sketch_object.kind.human_friendly_kind_with_article()
709 ))));
710 };
711 let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
712
713 self.scene_graph.sketch_mode = Some(sketch);
715
716 let mut truncated_program = self.program.clone();
718 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
719 .map_err(KclErrorWithOutputs::no_outputs)?;
720
721 let outcome = ctx
724 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
725 .await?;
726
727 let outcome = self.update_state_after_exec(outcome, true);
729 let scene_graph_delta = SceneGraphDelta {
730 new_graph: self.scene_graph_for_ui(),
731 invalidates_ids: false,
732 new_objects: Vec::new(),
733 exec_outcome: outcome,
734 };
735 Ok(scene_graph_delta)
736 }
737
738 async fn exit_sketch(
739 &mut self,
740 ctx: &ExecutorContext,
741 _version: Version,
742 sketch: ObjectId,
743 ) -> ExecResult<SceneGraph> {
744 #[cfg(not(target_arch = "wasm32"))]
746 let _ = sketch;
747 #[cfg(target_arch = "wasm32")]
748 if self.scene_graph.sketch_mode != Some(sketch) {
749 web_sys::console::warn_1(
750 &format!(
751 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
752 &self.scene_graph.sketch_mode
753 )
754 .into(),
755 );
756 }
757 self.scene_graph.sketch_mode = None;
758
759 let outcome = ctx.run_with_caching(self.program.clone()).await?;
761
762 self.update_state_after_exec(outcome, false);
764
765 Ok(self.scene_graph_for_ui())
766 }
767
768 async fn delete_sketch(
769 &mut self,
770 ctx: &ExecutorContext,
771 _version: Version,
772 sketch: ObjectId,
773 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
774 let mut new_ast = self.program.ast.clone();
777
778 let sketch_id = sketch;
780 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
781 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
782 })?;
783 let ObjectKind::Sketch(_) = &sketch_object.kind else {
784 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
785 "Object is not a sketch, it is {}",
786 sketch_object.kind.human_friendly_kind_with_article(),
787 ))));
788 };
789
790 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
792 .map_err(KclErrorWithOutputs::no_outputs)?;
793
794 self.execute_after_delete_sketch(ctx, &mut new_ast).await
795 }
796
797 async fn add_segment(
798 &mut self,
799 ctx: &ExecutorContext,
800 _version: Version,
801 sketch: ObjectId,
802 segment: SegmentCtor,
803 _label: Option<String>,
804 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
805 match segment {
807 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
808 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
809 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
810 SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
811 SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
812 }
813 }
814
815 async fn edit_segments(
816 &mut self,
817 ctx: &ExecutorContext,
818 _version: Version,
819 sketch: ObjectId,
820 segments: Vec<ExistingSegmentCtor>,
821 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
822 let sketch_block_ref =
824 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
825
826 let mut new_ast = self.program.ast.clone();
827 let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
828 let mut invalidates_ids = false;
829
830 for segment in &segments {
833 edited_segment_ids.insert(segment.id);
834 if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
835 && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
836 && let ObjectKind::Segment {
837 segment: Segment::ControlPointSpline(existing_spline),
838 } = &existing_object.kind
839 && existing_spline.controls.len() != new_ctor.points.len()
840 {
841 invalidates_ids = true;
842 }
843 }
844 let drag_anchor_segment_ids = self
845 .next_drag_anchor_segment_ids
846 .take()
847 .unwrap_or_else(|| edited_segment_ids.clone());
848 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
849
850 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
865
866 for segment in segments {
867 let segment_id = segment.id;
868 match segment.ctor {
869 SegmentCtor::Point(ctor) => {
870 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
872 && let ObjectKind::Segment { segment } = &segment_object.kind
873 && let Segment::Point(point) = segment
874 && let Some(owner_id) = point.owner
875 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
876 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
877 {
878 match owner_segment {
879 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
880 if let Some(existing) = final_edits.get_mut(&owner_id) {
881 let SegmentCtor::Line(line_ctor) = existing else {
882 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
883 "Internal: Expected line ctor for owner, but found {}",
884 existing.human_friendly_kind_with_article()
885 ))));
886 };
887 if line.start == segment_id {
889 line_ctor.start = ctor.position;
890 } else {
891 line_ctor.end = ctor.position;
892 }
893 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
894 let mut line_ctor = line_ctor.clone();
896 if line.start == segment_id {
897 line_ctor.start = ctor.position;
898 } else {
899 line_ctor.end = ctor.position;
900 }
901 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
902 } else {
903 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
905 "Internal: Line does not have line ctor, but found {}",
906 line.ctor.human_friendly_kind_with_article()
907 ))));
908 }
909 continue;
910 }
911 Segment::Arc(arc)
912 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
913 {
914 if let Some(existing) = final_edits.get_mut(&owner_id) {
915 let SegmentCtor::Arc(arc_ctor) = existing else {
916 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
917 "Internal: Expected arc ctor for owner, but found {}",
918 existing.human_friendly_kind_with_article()
919 ))));
920 };
921 if arc.start == segment_id {
922 arc_ctor.start = ctor.position;
923 } else if arc.end == segment_id {
924 arc_ctor.end = ctor.position;
925 } else {
926 arc_ctor.center = ctor.position;
927 }
928 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
929 let mut arc_ctor = arc_ctor.clone();
930 if arc.start == segment_id {
931 arc_ctor.start = ctor.position;
932 } else if arc.end == segment_id {
933 arc_ctor.end = ctor.position;
934 } else {
935 arc_ctor.center = ctor.position;
936 }
937 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
938 } else {
939 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
940 "Internal: Arc does not have arc ctor, but found {}",
941 arc.ctor.human_friendly_kind_with_article()
942 ))));
943 }
944 continue;
945 }
946 Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
947 if let Some(existing) = final_edits.get_mut(&owner_id) {
948 let SegmentCtor::Circle(circle_ctor) = existing else {
949 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
950 "Internal: Expected circle ctor for owner, but found {}",
951 existing.human_friendly_kind_with_article()
952 ))));
953 };
954 if circle.start == segment_id {
955 circle_ctor.start = ctor.position;
956 } else {
957 circle_ctor.center = ctor.position;
958 }
959 } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
960 let mut circle_ctor = circle_ctor.clone();
961 if circle.start == segment_id {
962 circle_ctor.start = ctor.position;
963 } else {
964 circle_ctor.center = ctor.position;
965 }
966 final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
967 } else {
968 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
969 "Internal: Circle does not have circle ctor, but found {}",
970 circle.ctor.human_friendly_kind_with_article()
971 ))));
972 }
973 continue;
974 }
975 Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
976 let Some(control_index) =
977 spline.controls.iter().position(|control_id| *control_id == segment_id)
978 else {
979 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
980 "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
981 ))));
982 };
983 if let Some(existing) = final_edits.get_mut(&owner_id) {
984 let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
985 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
986 "Internal: Expected controlPointSpline ctor for owner, but found {}",
987 existing.human_friendly_kind_with_article()
988 ))));
989 };
990 spline_ctor.points[control_index] = ctor.position;
991 } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
992 let mut spline_ctor = spline_ctor.clone();
993 spline_ctor.points[control_index] = ctor.position;
994 final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
995 } else {
996 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
997 "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
998 spline.ctor.human_friendly_kind_with_article()
999 ))));
1000 }
1001 continue;
1002 }
1003 _ => {}
1004 }
1005 }
1006
1007 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1009 }
1010 SegmentCtor::Line(ctor) => {
1011 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1012 }
1013 SegmentCtor::Arc(ctor) => {
1014 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1015 }
1016 SegmentCtor::Circle(ctor) => {
1017 final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1018 }
1019 SegmentCtor::ControlPointSpline(ctor) => {
1020 final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1021 }
1022 }
1023 }
1024
1025 for (segment_id, ctor) in final_edits {
1026 match ctor {
1027 SegmentCtor::Point(ctor) => self
1028 .edit_point(&mut new_ast, sketch, segment_id, ctor)
1029 .map_err(KclErrorWithOutputs::no_outputs)?,
1030 SegmentCtor::Line(ctor) => self
1031 .edit_line(&mut new_ast, sketch, segment_id, ctor)
1032 .map_err(KclErrorWithOutputs::no_outputs)?,
1033 SegmentCtor::Arc(ctor) => self
1034 .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1035 .map_err(KclErrorWithOutputs::no_outputs)?,
1036 SegmentCtor::Circle(ctor) => self
1037 .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1038 .map_err(KclErrorWithOutputs::no_outputs)?,
1039 SegmentCtor::ControlPointSpline(ctor) => self
1040 .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1041 .map_err(KclErrorWithOutputs::no_outputs)?,
1042 }
1043 }
1044 let (source_delta, mut scene_graph_delta) = self
1045 .execute_after_edit(
1046 ctx,
1047 sketch,
1048 sketch_block_ref,
1049 &mut new_ast,
1050 ExecuteAfterEditOptions {
1051 segment_ids_edited: drag_anchor_segment_ids,
1052 edit_kind: EditDeleteKind::Edit,
1053 commit_solved_initial_guesses,
1054 },
1055 )
1056 .await?;
1057 if invalidates_ids {
1058 scene_graph_delta.invalidates_ids = true;
1059 }
1060 Ok((source_delta, scene_graph_delta))
1061 }
1062
1063 async fn delete_objects(
1064 &mut self,
1065 ctx: &ExecutorContext,
1066 _version: Version,
1067 sketch: ObjectId,
1068 constraint_ids: Vec<ObjectId>,
1069 segment_ids: Vec<ObjectId>,
1070 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1071 let sketch_block_ref =
1073 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1074
1075 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1077 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1078
1079 let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1082
1083 for segment_id in segment_ids_set.iter().copied() {
1084 let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1085 let ObjectKind::Segment { segment } = &segment_object.kind else {
1086 return None;
1087 };
1088 match segment {
1089 Segment::Point(point) => point.owner,
1090 Segment::Line(line) => line.owner,
1091 _ => None,
1092 }
1093 });
1094
1095 if let Some(owner_id) = owner_id
1096 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1097 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1098 && matches!(
1099 owner_segment,
1100 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1101 )
1102 {
1103 resolved_segment_ids_to_delete.insert(owner_id);
1105 } else {
1106 resolved_segment_ids_to_delete.insert(segment_id);
1108 }
1109 }
1110 let referenced_constraint_ids = self
1111 .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1112 .map_err(KclErrorWithOutputs::no_outputs)?;
1113
1114 let mut new_ast = self.program.ast.clone();
1115
1116 for constraint_id in referenced_constraint_ids {
1117 if constraint_ids_set.contains(&constraint_id) {
1118 continue;
1119 }
1120
1121 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1122 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1123 })?;
1124 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1125 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1126 "Object is not a constraint, it is {}",
1127 constraint_object.kind.human_friendly_kind_with_article()
1128 ))));
1129 };
1130
1131 match constraint {
1132 Constraint::Coincident(coincident) => {
1133 let remaining_segments =
1134 self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1135
1136 if remaining_segments.len() >= 2 {
1138 self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1139 .map_err(KclErrorWithOutputs::no_outputs)?;
1140 } else {
1141 constraint_ids_set.insert(constraint_id);
1142 }
1143 }
1144 Constraint::EqualRadius(equal_radius) => {
1145 let remaining_input = equal_radius
1146 .input
1147 .iter()
1148 .copied()
1149 .filter(|segment_id| {
1150 !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1151 })
1152 .collect::<Vec<_>>();
1153
1154 if remaining_input.len() >= 2 {
1155 self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1156 .map_err(KclErrorWithOutputs::no_outputs)?;
1157 } else {
1158 constraint_ids_set.insert(constraint_id);
1159 }
1160 }
1161 Constraint::LinesEqualLength(lines_equal_length) => {
1162 let remaining_lines = lines_equal_length
1163 .lines
1164 .iter()
1165 .copied()
1166 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1167 .collect::<Vec<_>>();
1168
1169 if remaining_lines.len() >= 2 {
1171 self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1172 .map_err(KclErrorWithOutputs::no_outputs)?;
1173 } else {
1174 constraint_ids_set.insert(constraint_id);
1175 }
1176 }
1177 Constraint::Parallel(parallel) => {
1178 let remaining_lines = parallel
1179 .lines
1180 .iter()
1181 .copied()
1182 .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1183 .collect::<Vec<_>>();
1184
1185 if remaining_lines.len() >= 2 {
1186 self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1187 .map_err(KclErrorWithOutputs::no_outputs)?;
1188 } else {
1189 constraint_ids_set.insert(constraint_id);
1190 }
1191 }
1192 Constraint::Horizontal(Horizontal::Points { points }) => {
1193 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1194
1195 if remaining_points.len() >= 2 {
1196 self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1197 .map_err(KclErrorWithOutputs::no_outputs)?;
1198 } else {
1199 constraint_ids_set.insert(constraint_id);
1200 }
1201 }
1202 Constraint::Vertical(Vertical::Points { points }) => {
1203 let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1204
1205 if remaining_points.len() >= 2 {
1206 self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1207 .map_err(KclErrorWithOutputs::no_outputs)?;
1208 } else {
1209 constraint_ids_set.insert(constraint_id);
1210 }
1211 }
1212 Constraint::Fixed(fixed) => {
1213 if fixed.points.iter().any(|fixed_point| {
1214 self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1215 }) {
1216 constraint_ids_set.insert(constraint_id);
1217 }
1218 }
1219 _ => {
1220 constraint_ids_set.insert(constraint_id);
1222 }
1223 }
1224 }
1225
1226 for constraint_id in constraint_ids_set {
1227 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1228 .map_err(KclErrorWithOutputs::no_outputs)?;
1229 }
1230 for segment_id in resolved_segment_ids_to_delete {
1231 self.delete_segment(&mut new_ast, sketch, segment_id)
1232 .map_err(KclErrorWithOutputs::no_outputs)?;
1233 }
1234
1235 self.execute_after_edit(
1236 ctx,
1237 sketch,
1238 sketch_block_ref,
1239 &mut new_ast,
1240 ExecuteAfterEditOptions {
1241 segment_ids_edited: Default::default(),
1242 edit_kind: EditDeleteKind::DeleteNonSketch,
1243 commit_solved_initial_guesses: true,
1244 },
1245 )
1246 .await
1247 }
1248
1249 async fn add_constraint(
1250 &mut self,
1251 ctx: &ExecutorContext,
1252 _version: Version,
1253 sketch: ObjectId,
1254 constraint: Constraint,
1255 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1256 let original_program = self.program.clone();
1260 let original_scene_graph = self.scene_graph.clone();
1261
1262 let mut new_ast = self.program.ast.clone();
1263 let sketch_block_ref = match constraint {
1264 Constraint::Coincident(coincident) => self
1265 .add_coincident(sketch, coincident, &mut new_ast)
1266 .await
1267 .map_err(KclErrorWithOutputs::no_outputs)?,
1268 Constraint::Distance(distance) => self
1269 .add_distance(sketch, distance, &mut new_ast)
1270 .await
1271 .map_err(KclErrorWithOutputs::no_outputs)?,
1272 Constraint::EqualRadius(equal_radius) => self
1273 .add_equal_radius(sketch, equal_radius, &mut new_ast)
1274 .await
1275 .map_err(KclErrorWithOutputs::no_outputs)?,
1276 Constraint::Fixed(fixed) => self
1277 .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1278 .await
1279 .map_err(KclErrorWithOutputs::no_outputs)?,
1280 Constraint::HorizontalDistance(distance) => self
1281 .add_horizontal_distance(sketch, distance, &mut new_ast)
1282 .await
1283 .map_err(KclErrorWithOutputs::no_outputs)?,
1284 Constraint::VerticalDistance(distance) => self
1285 .add_vertical_distance(sketch, distance, &mut new_ast)
1286 .await
1287 .map_err(KclErrorWithOutputs::no_outputs)?,
1288 Constraint::Horizontal(horizontal) => self
1289 .add_horizontal(sketch, horizontal, &mut new_ast)
1290 .await
1291 .map_err(KclErrorWithOutputs::no_outputs)?,
1292 Constraint::LinesEqualLength(lines_equal_length) => self
1293 .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1294 .await
1295 .map_err(KclErrorWithOutputs::no_outputs)?,
1296 Constraint::Midpoint(midpoint) => self
1297 .add_midpoint(sketch, midpoint, &mut new_ast)
1298 .await
1299 .map_err(KclErrorWithOutputs::no_outputs)?,
1300 Constraint::Parallel(parallel) => self
1301 .add_parallel(sketch, parallel, &mut new_ast)
1302 .await
1303 .map_err(KclErrorWithOutputs::no_outputs)?,
1304 Constraint::Perpendicular(perpendicular) => self
1305 .add_perpendicular(sketch, perpendicular, &mut new_ast)
1306 .await
1307 .map_err(KclErrorWithOutputs::no_outputs)?,
1308 Constraint::Radius(radius) => self
1309 .add_radius(sketch, radius, &mut new_ast)
1310 .await
1311 .map_err(KclErrorWithOutputs::no_outputs)?,
1312 Constraint::Diameter(diameter) => self
1313 .add_diameter(sketch, diameter, &mut new_ast)
1314 .await
1315 .map_err(KclErrorWithOutputs::no_outputs)?,
1316 Constraint::Symmetric(symmetric) => self
1317 .add_symmetric(sketch, symmetric, &mut new_ast)
1318 .await
1319 .map_err(KclErrorWithOutputs::no_outputs)?,
1320 Constraint::Vertical(vertical) => self
1321 .add_vertical(sketch, vertical, &mut new_ast)
1322 .await
1323 .map_err(KclErrorWithOutputs::no_outputs)?,
1324 Constraint::Angle(lines_at_angle) => self
1325 .add_angle(sketch, lines_at_angle, &mut new_ast)
1326 .await
1327 .map_err(KclErrorWithOutputs::no_outputs)?,
1328 Constraint::Tangent(tangent) => self
1329 .add_tangent(sketch, tangent, &mut new_ast)
1330 .await
1331 .map_err(KclErrorWithOutputs::no_outputs)?,
1332 };
1333
1334 let result = self
1335 .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1336 .await;
1337
1338 if result.is_err() {
1340 self.program = original_program;
1341 self.scene_graph = original_scene_graph;
1342 }
1343
1344 result
1345 }
1346
1347 async fn chain_segment(
1348 &mut self,
1349 ctx: &ExecutorContext,
1350 version: Version,
1351 sketch: ObjectId,
1352 previous_segment_end_point_id: ObjectId,
1353 segment: SegmentCtor,
1354 _label: Option<String>,
1355 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1356 let SegmentCtor::Line(line_ctor) = segment else {
1360 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1361 "chain_segment currently only supports Line segments, got {}",
1362 segment.human_friendly_kind_with_article(),
1363 ))));
1364 };
1365
1366 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1368
1369 let new_line_id = first_scene_delta
1372 .new_objects
1373 .iter()
1374 .find(|&obj_id| {
1375 let obj = self.scene_graph.objects.get(obj_id.0);
1376 if let Some(obj) = obj {
1377 matches!(
1378 &obj.kind,
1379 ObjectKind::Segment {
1380 segment: Segment::Line(_)
1381 }
1382 )
1383 } else {
1384 false
1385 }
1386 })
1387 .ok_or_else(|| {
1388 KclErrorWithOutputs::no_outputs(KclError::refactor(
1389 "Failed to find new line segment in scene graph".to_string(),
1390 ))
1391 })?;
1392
1393 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1394 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1395 "New line object not found: {new_line_id:?}"
1396 )))
1397 })?;
1398
1399 let ObjectKind::Segment {
1400 segment: new_line_segment,
1401 } = &new_line_obj.kind
1402 else {
1403 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1404 "Object is not a segment: {new_line_obj:?}"
1405 ))));
1406 };
1407
1408 let Segment::Line(new_line) = new_line_segment else {
1409 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1410 "Segment is not a line: {new_line_segment:?}"
1411 ))));
1412 };
1413
1414 let new_line_start_point_id = new_line.start;
1415
1416 let coincident = Coincident {
1418 segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1419 };
1420
1421 let (final_src_delta, final_scene_delta) = self
1422 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1423 .await?;
1424
1425 let mut combined_new_objects = first_scene_delta.new_objects.clone();
1428 combined_new_objects.extend(final_scene_delta.new_objects);
1429
1430 let scene_graph_delta = SceneGraphDelta {
1431 new_graph: self.scene_graph_for_ui(),
1432 invalidates_ids: false,
1433 new_objects: combined_new_objects,
1434 exec_outcome: final_scene_delta.exec_outcome,
1435 };
1436
1437 Ok((final_src_delta, scene_graph_delta))
1438 }
1439
1440 async fn edit_constraint(
1441 &mut self,
1442 ctx: &ExecutorContext,
1443 _version: Version,
1444 sketch: ObjectId,
1445 constraint_id: ObjectId,
1446 value_expression: String,
1447 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1448 let sketch_block_ref =
1450 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1451
1452 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1453 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1454 })?;
1455 if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1456 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1457 "Object is not a constraint: {constraint_id:?}"
1458 ))));
1459 }
1460
1461 let mut new_ast = self.program.ast.clone();
1462
1463 let (parsed, errors) = Program::parse(&value_expression)
1465 .map_err(|e| KclErrorWithOutputs::no_outputs(KclError::refactor(e.to_string())))?;
1466 if !errors.is_empty() {
1467 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1468 "Error parsing value expression: {errors:?}"
1469 ))));
1470 }
1471 let mut parsed = parsed.ok_or_else(|| {
1472 KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1473 })?;
1474 if parsed.ast.body.is_empty() {
1475 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1476 "Empty value expression".to_string(),
1477 )));
1478 }
1479 let first = parsed.ast.body.remove(0);
1480 let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1481 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1482 "Value expression must be a simple expression".to_string(),
1483 )));
1484 };
1485
1486 let new_value: ast::BinaryPart = expr_stmt
1487 .inner
1488 .expression
1489 .try_into()
1490 .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1491
1492 self.mutate_ast(
1493 &mut new_ast,
1494 constraint_id,
1495 AstMutateCommand::EditConstraintValue { value: new_value },
1496 )
1497 .map_err(KclErrorWithOutputs::no_outputs)?;
1498
1499 self.execute_after_edit(
1500 ctx,
1501 sketch,
1502 sketch_block_ref,
1503 &mut new_ast,
1504 ExecuteAfterEditOptions {
1505 segment_ids_edited: Default::default(),
1506 edit_kind: EditDeleteKind::Edit,
1507 commit_solved_initial_guesses: true,
1508 },
1509 )
1510 .await
1511 }
1512
1513 async fn edit_distance_constraint_label_position(
1514 &mut self,
1515 ctx: &ExecutorContext,
1516 _version: Version,
1517 sketch: ObjectId,
1518 constraint_id: ObjectId,
1519 label_position: Point2d<Number>,
1520 anchor_segment_ids: Vec<ObjectId>,
1521 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1522 let sketch_block_ref =
1524 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1525
1526 let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1527 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1528 })?;
1529 if !matches!(
1530 &object.kind,
1531 ObjectKind::Constraint {
1532 constraint: Constraint::Distance(_)
1533 | Constraint::HorizontalDistance(_)
1534 | Constraint::VerticalDistance(_)
1535 | Constraint::Radius(_)
1536 | Constraint::Diameter(_),
1537 }
1538 ) {
1539 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1540 "Object does not support labelPosition: {constraint_id:?}"
1541 ))));
1542 }
1543
1544 let label_position = to_ast_point2d_number(&label_position).map_err(|err| {
1545 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1546 "Could not convert label position to AST: {err}"
1547 )))
1548 })?;
1549 let mut new_ast = self.program.ast.clone();
1550 self.mutate_ast(
1551 &mut new_ast,
1552 constraint_id,
1553 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
1554 )
1555 .map_err(KclErrorWithOutputs::no_outputs)?;
1556 let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1557
1558 self.execute_after_edit(
1559 ctx,
1560 sketch,
1561 sketch_block_ref,
1562 &mut new_ast,
1563 ExecuteAfterEditOptions {
1564 segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1565 edit_kind: EditDeleteKind::Edit,
1566 commit_solved_initial_guesses,
1567 },
1568 )
1569 .await
1570 }
1571
1572 async fn batch_split_segment_operations(
1580 &mut self,
1581 ctx: &ExecutorContext,
1582 _version: Version,
1583 sketch: ObjectId,
1584 edit_segments: Vec<ExistingSegmentCtor>,
1585 add_constraints: Vec<Constraint>,
1586 delete_constraint_ids: Vec<ObjectId>,
1587 _new_segment_info: sketch::NewSegmentInfo,
1588 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1589 let sketch_block_ref =
1591 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1592
1593 let mut new_ast = self.program.ast.clone();
1594 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1595
1596 for segment in edit_segments {
1598 segment_ids_edited.insert(segment.id);
1599 match segment.ctor {
1600 SegmentCtor::Point(ctor) => self
1601 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1602 .map_err(KclErrorWithOutputs::no_outputs)?,
1603 SegmentCtor::Line(ctor) => self
1604 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1605 .map_err(KclErrorWithOutputs::no_outputs)?,
1606 SegmentCtor::Arc(ctor) => self
1607 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1608 .map_err(KclErrorWithOutputs::no_outputs)?,
1609 SegmentCtor::Circle(ctor) => self
1610 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1611 .map_err(KclErrorWithOutputs::no_outputs)?,
1612 SegmentCtor::ControlPointSpline(ctor) => self
1613 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1614 .map_err(KclErrorWithOutputs::no_outputs)?,
1615 }
1616 }
1617
1618 for constraint in add_constraints {
1620 match constraint {
1621 Constraint::Coincident(coincident) => {
1622 self.add_coincident(sketch, coincident, &mut new_ast)
1623 .await
1624 .map_err(KclErrorWithOutputs::no_outputs)?;
1625 }
1626 Constraint::Distance(distance) => {
1627 self.add_distance(sketch, distance, &mut new_ast)
1628 .await
1629 .map_err(KclErrorWithOutputs::no_outputs)?;
1630 }
1631 Constraint::EqualRadius(equal_radius) => {
1632 self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1633 .await
1634 .map_err(KclErrorWithOutputs::no_outputs)?;
1635 }
1636 Constraint::Fixed(fixed) => {
1637 self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1638 .await
1639 .map_err(KclErrorWithOutputs::no_outputs)?;
1640 }
1641 Constraint::HorizontalDistance(distance) => {
1642 self.add_horizontal_distance(sketch, distance, &mut new_ast)
1643 .await
1644 .map_err(KclErrorWithOutputs::no_outputs)?;
1645 }
1646 Constraint::VerticalDistance(distance) => {
1647 self.add_vertical_distance(sketch, distance, &mut new_ast)
1648 .await
1649 .map_err(KclErrorWithOutputs::no_outputs)?;
1650 }
1651 Constraint::Horizontal(horizontal) => {
1652 self.add_horizontal(sketch, horizontal, &mut new_ast)
1653 .await
1654 .map_err(KclErrorWithOutputs::no_outputs)?;
1655 }
1656 Constraint::LinesEqualLength(lines_equal_length) => {
1657 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1658 .await
1659 .map_err(KclErrorWithOutputs::no_outputs)?;
1660 }
1661 Constraint::Midpoint(midpoint) => {
1662 self.add_midpoint(sketch, midpoint, &mut new_ast)
1663 .await
1664 .map_err(KclErrorWithOutputs::no_outputs)?;
1665 }
1666 Constraint::Parallel(parallel) => {
1667 self.add_parallel(sketch, parallel, &mut new_ast)
1668 .await
1669 .map_err(KclErrorWithOutputs::no_outputs)?;
1670 }
1671 Constraint::Perpendicular(perpendicular) => {
1672 self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1673 .await
1674 .map_err(KclErrorWithOutputs::no_outputs)?;
1675 }
1676 Constraint::Vertical(vertical) => {
1677 self.add_vertical(sketch, vertical, &mut new_ast)
1678 .await
1679 .map_err(KclErrorWithOutputs::no_outputs)?;
1680 }
1681 Constraint::Diameter(diameter) => {
1682 self.add_diameter(sketch, diameter, &mut new_ast)
1683 .await
1684 .map_err(KclErrorWithOutputs::no_outputs)?;
1685 }
1686 Constraint::Radius(radius) => {
1687 self.add_radius(sketch, radius, &mut new_ast)
1688 .await
1689 .map_err(KclErrorWithOutputs::no_outputs)?;
1690 }
1691 Constraint::Symmetric(symmetric) => {
1692 self.add_symmetric(sketch, symmetric, &mut new_ast)
1693 .await
1694 .map_err(KclErrorWithOutputs::no_outputs)?;
1695 }
1696 Constraint::Angle(angle) => {
1697 self.add_angle(sketch, angle, &mut new_ast)
1698 .await
1699 .map_err(KclErrorWithOutputs::no_outputs)?;
1700 }
1701 Constraint::Tangent(tangent) => {
1702 self.add_tangent(sketch, tangent, &mut new_ast)
1703 .await
1704 .map_err(KclErrorWithOutputs::no_outputs)?;
1705 }
1706 }
1707 }
1708
1709 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1711
1712 let has_constraint_deletions = !constraint_ids_set.is_empty();
1713 for constraint_id in constraint_ids_set {
1714 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1715 .map_err(KclErrorWithOutputs::no_outputs)?;
1716 }
1717
1718 let (source_delta, mut scene_graph_delta) = self
1722 .execute_after_edit(
1723 ctx,
1724 sketch,
1725 sketch_block_ref,
1726 &mut new_ast,
1727 ExecuteAfterEditOptions {
1728 segment_ids_edited,
1729 edit_kind: EditDeleteKind::Edit,
1730 commit_solved_initial_guesses: true,
1731 },
1732 )
1733 .await?;
1734
1735 if has_constraint_deletions {
1738 scene_graph_delta.invalidates_ids = true;
1739 }
1740
1741 Ok((source_delta, scene_graph_delta))
1742 }
1743
1744 async fn batch_tail_cut_operations(
1745 &mut self,
1746 ctx: &ExecutorContext,
1747 _version: Version,
1748 sketch: ObjectId,
1749 edit_segments: Vec<ExistingSegmentCtor>,
1750 add_constraints: Vec<Constraint>,
1751 delete_constraint_ids: Vec<ObjectId>,
1752 additional_edited_segment_ids: Vec<ObjectId>,
1753 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1754 let sketch_block_ref =
1755 sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1756
1757 let mut new_ast = self.program.ast.clone();
1758 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1759
1760 for segment in edit_segments {
1762 segment_ids_edited.insert(segment.id);
1763 match segment.ctor {
1764 SegmentCtor::Point(ctor) => self
1765 .edit_point(&mut new_ast, sketch, segment.id, ctor)
1766 .map_err(KclErrorWithOutputs::no_outputs)?,
1767 SegmentCtor::Line(ctor) => self
1768 .edit_line(&mut new_ast, sketch, segment.id, ctor)
1769 .map_err(KclErrorWithOutputs::no_outputs)?,
1770 SegmentCtor::Arc(ctor) => self
1771 .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1772 .map_err(KclErrorWithOutputs::no_outputs)?,
1773 SegmentCtor::Circle(ctor) => self
1774 .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1775 .map_err(KclErrorWithOutputs::no_outputs)?,
1776 SegmentCtor::ControlPointSpline(ctor) => self
1777 .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1778 .map_err(KclErrorWithOutputs::no_outputs)?,
1779 }
1780 }
1781
1782 segment_ids_edited.extend(additional_edited_segment_ids);
1783
1784 for constraint in add_constraints {
1786 match constraint {
1787 Constraint::Coincident(coincident) => {
1788 self.add_coincident(sketch, coincident, &mut new_ast)
1789 .await
1790 .map_err(KclErrorWithOutputs::no_outputs)?;
1791 }
1792 other => {
1793 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1794 "unsupported constraint in tail cut batch: {other:?}"
1795 ))));
1796 }
1797 }
1798 }
1799
1800 let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1802
1803 let has_constraint_deletions = !constraint_ids_set.is_empty();
1804 for constraint_id in constraint_ids_set {
1805 self.delete_constraint(&mut new_ast, sketch, constraint_id)
1806 .map_err(KclErrorWithOutputs::no_outputs)?;
1807 }
1808
1809 let (source_delta, mut scene_graph_delta) = self
1813 .execute_after_edit(
1814 ctx,
1815 sketch,
1816 sketch_block_ref,
1817 &mut new_ast,
1818 ExecuteAfterEditOptions {
1819 segment_ids_edited,
1820 edit_kind: EditDeleteKind::Edit,
1821 commit_solved_initial_guesses: true,
1822 },
1823 )
1824 .await?;
1825
1826 if has_constraint_deletions {
1829 scene_graph_delta.invalidates_ids = true;
1830 }
1831
1832 Ok((source_delta, scene_graph_delta))
1833 }
1834}
1835
1836impl FrontendState {
1837 pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1838 self.program = program.clone();
1839
1840 self.point_freedom_cache.clear();
1851 match ctx.run_with_caching(program).await {
1852 Ok(outcome) => {
1853 let outcome = self.update_state_after_exec(outcome, true);
1854 let checkpoint_id = self
1855 .create_sketch_checkpoint(outcome.clone())
1856 .await
1857 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1858 Ok(SetProgramOutcome::Success {
1859 scene_graph: Box::new(self.scene_graph_for_ui()),
1860 exec_outcome: Box::new(outcome),
1861 checkpoint_id: Some(checkpoint_id),
1862 })
1863 }
1864 Err(mut err) => {
1865 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1868 self.update_state_after_exec(outcome, true);
1869 err.scene_graph = Some(self.scene_graph_for_ui());
1870 Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
1871 }
1872 }
1873 }
1874
1875 pub async fn engine_execute(
1878 &mut self,
1879 ctx: &ExecutorContext,
1880 program: Program,
1881 ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
1882 self.program = program.clone();
1883
1884 self.point_freedom_cache.clear();
1888 match ctx.run_with_caching(program).await {
1889 Ok(outcome) => {
1890 let outcome = self.update_state_after_exec(outcome, true);
1891 Ok(SceneGraphDelta {
1892 new_graph: self.scene_graph_for_ui(),
1893 exec_outcome: outcome,
1894 new_objects: Default::default(),
1896 invalidates_ids: Default::default(),
1898 })
1899 }
1900 Err(mut err) => {
1901 let outcome = self.exec_outcome_from_exec_error(err.clone())?;
1903 self.update_state_after_exec(outcome, true);
1904 err.scene_graph = Some(self.scene_graph_for_ui());
1905 Err(err)
1906 }
1907 }
1908 }
1909
1910 fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
1911 if matches!(err.error, KclError::EngineHangup { .. }) {
1912 return Err(err);
1916 }
1917
1918 let KclErrorWithOutputs {
1919 error,
1920 mut non_fatal,
1921 variables,
1922 operations,
1923 artifact_graph,
1924 scene_objects,
1925 source_range_to_object,
1926 var_solutions,
1927 filenames,
1928 default_planes,
1929 ..
1930 } = err;
1931
1932 if let Some(source_range) = error.source_ranges().first() {
1933 non_fatal.push(CompilationIssue::fatal(*source_range, error.get_message()));
1934 } else {
1935 non_fatal.push(CompilationIssue::fatal(SourceRange::synthetic(), error.get_message()));
1936 }
1937
1938 Ok(ExecOutcome {
1939 variables,
1940 filenames,
1941 operations,
1942 artifact_graph,
1943 scene_objects,
1944 source_range_to_object,
1945 var_solutions,
1946 issues: non_fatal,
1947 default_planes,
1948 })
1949 }
1950
1951 async fn add_point(
1952 &mut self,
1953 ctx: &ExecutorContext,
1954 sketch: ObjectId,
1955 ctor: PointCtor,
1956 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1957 let at_ast = to_ast_point2d(&ctor.position)
1959 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
1960 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1961 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
1962 unlabeled: None,
1963 arguments: vec![ast::LabeledArg {
1964 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
1965 arg: at_ast,
1966 }],
1967 digest: None,
1968 non_code_meta: Default::default(),
1969 })));
1970
1971 let sketch_id = sketch;
1973 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
1974 #[cfg(target_arch = "wasm32")]
1975 web_sys::console::error_1(
1976 &format!(
1977 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
1978 &self.scene_graph.objects
1979 )
1980 .into(),
1981 );
1982 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
1983 })?;
1984 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1985 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1986 "Object is not a sketch, it is {}",
1987 sketch_object.kind.human_friendly_kind_with_article(),
1988 ))));
1989 };
1990 let mut new_ast = self.program.ast.clone();
1992 let (sketch_block_ref, _) = self
1993 .mutate_ast(
1994 &mut new_ast,
1995 sketch_id,
1996 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1997 )
1998 .map_err(KclErrorWithOutputs::no_outputs)?;
1999 let new_source = source_from_ast(&new_ast);
2001 let (new_program, errors) = Program::parse(&new_source)
2003 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2004 if !errors.is_empty() {
2005 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2006 "Error parsing KCL source after adding point: {errors:?}"
2007 ))));
2008 }
2009 let Some(new_program) = new_program else {
2010 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2011 "No AST produced after adding point".to_string(),
2012 )));
2013 };
2014
2015 let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2016 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2017 "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2018 )))
2019 })?;
2020
2021 self.program = new_program.clone();
2023
2024 let mut truncated_program = new_program;
2026 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2027 .map_err(KclErrorWithOutputs::no_outputs)?;
2028
2029 let outcome = ctx
2031 .run_mock(
2032 &truncated_program,
2033 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2034 )
2035 .await?;
2036
2037 let new_object_ids = {
2038 let make_err =
2039 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2040 let segment_id = outcome
2041 .source_range_to_object
2042 .get(&point_node_ref.range)
2043 .copied()
2044 .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2045 let segment_object = outcome
2046 .scene_objects
2047 .get(segment_id.0)
2048 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2049 let ObjectKind::Segment { segment } = &segment_object.kind else {
2050 return Err(make_err(format!(
2051 "Object is not a segment, it is {}",
2052 segment_object.kind.human_friendly_kind_with_article()
2053 )));
2054 };
2055 let Segment::Point(_) = segment else {
2056 return Err(make_err(format!(
2057 "Segment is not a point, it is {}",
2058 segment.human_friendly_kind_with_article()
2059 )));
2060 };
2061 vec![segment_id]
2062 };
2063 let src_delta = SourceDelta { text: new_source };
2064 let outcome = self.update_state_after_exec(outcome, false);
2066 let scene_graph_delta = SceneGraphDelta {
2067 new_graph: self.scene_graph_for_ui(),
2068 invalidates_ids: false,
2069 new_objects: new_object_ids,
2070 exec_outcome: outcome,
2071 };
2072 Ok((src_delta, scene_graph_delta))
2073 }
2074
2075 async fn add_line(
2076 &mut self,
2077 ctx: &ExecutorContext,
2078 sketch: ObjectId,
2079 ctor: LineCtor,
2080 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2081 let start_ast = to_ast_point2d(&ctor.start)
2083 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2084 let end_ast = to_ast_point2d(&ctor.end)
2085 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2086 let mut arguments = vec![
2087 ast::LabeledArg {
2088 label: Some(ast::Identifier::new(LINE_START_PARAM)),
2089 arg: start_ast,
2090 },
2091 ast::LabeledArg {
2092 label: Some(ast::Identifier::new(LINE_END_PARAM)),
2093 arg: end_ast,
2094 },
2095 ];
2096 if ctor.construction == Some(true) {
2098 arguments.push(ast::LabeledArg {
2099 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2100 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2101 value: ast::LiteralValue::Bool(true),
2102 raw: "true".to_string(),
2103 digest: None,
2104 }))),
2105 });
2106 }
2107 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2108 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2109 unlabeled: None,
2110 arguments,
2111 digest: None,
2112 non_code_meta: Default::default(),
2113 })));
2114
2115 let sketch_id = sketch;
2117 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2118 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2119 })?;
2120 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2121 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2122 "Object is not a sketch, it is {}",
2123 sketch_object.kind.human_friendly_kind_with_article(),
2124 ))));
2125 };
2126 let mut new_ast = self.program.ast.clone();
2128 let (sketch_block_ref, _) = self
2129 .mutate_ast(
2130 &mut new_ast,
2131 sketch_id,
2132 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2133 )
2134 .map_err(KclErrorWithOutputs::no_outputs)?;
2135 let new_source = source_from_ast(&new_ast);
2137 let (new_program, errors) = Program::parse(&new_source)
2139 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2140 if !errors.is_empty() {
2141 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2142 "Error parsing KCL source after adding line: {errors:?}"
2143 ))));
2144 }
2145 let Some(new_program) = new_program else {
2146 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2147 "No AST produced after adding line".to_string(),
2148 )));
2149 };
2150
2151 let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2152 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2153 "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2154 )))
2155 })?;
2156
2157 self.program = new_program.clone();
2159
2160 let mut truncated_program = new_program;
2162 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2163 .map_err(KclErrorWithOutputs::no_outputs)?;
2164
2165 let outcome = ctx
2167 .run_mock(
2168 &truncated_program,
2169 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2170 )
2171 .await?;
2172
2173 let new_object_ids = {
2174 let make_err =
2175 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2176 let segment_id = outcome
2177 .source_range_to_object
2178 .get(&line_node_ref.range)
2179 .copied()
2180 .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2181 let segment_object = outcome
2182 .scene_object_by_id(segment_id)
2183 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2184 let ObjectKind::Segment { segment } = &segment_object.kind else {
2185 return Err(make_err(format!(
2186 "Object is not a segment, it is {}",
2187 segment_object.kind.human_friendly_kind_with_article()
2188 )));
2189 };
2190 let Segment::Line(line) = segment else {
2191 return Err(make_err(format!(
2192 "Segment is not a line, it is {}",
2193 segment.human_friendly_kind_with_article()
2194 )));
2195 };
2196 vec![line.start, line.end, segment_id]
2197 };
2198 let src_delta = SourceDelta { text: new_source };
2199 let outcome = self.update_state_after_exec(outcome, false);
2201 let scene_graph_delta = SceneGraphDelta {
2202 new_graph: self.scene_graph_for_ui(),
2203 invalidates_ids: false,
2204 new_objects: new_object_ids,
2205 exec_outcome: outcome,
2206 };
2207 Ok((src_delta, scene_graph_delta))
2208 }
2209
2210 async fn add_arc(
2211 &mut self,
2212 ctx: &ExecutorContext,
2213 sketch: ObjectId,
2214 ctor: ArcCtor,
2215 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2216 let start_ast = to_ast_point2d(&ctor.start)
2218 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2219 let end_ast = to_ast_point2d(&ctor.end)
2220 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2221 let center_ast = to_ast_point2d(&ctor.center)
2222 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2223 let mut arguments = vec![
2224 ast::LabeledArg {
2225 label: Some(ast::Identifier::new(ARC_START_PARAM)),
2226 arg: start_ast,
2227 },
2228 ast::LabeledArg {
2229 label: Some(ast::Identifier::new(ARC_END_PARAM)),
2230 arg: end_ast,
2231 },
2232 ast::LabeledArg {
2233 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2234 arg: center_ast,
2235 },
2236 ];
2237 if ctor.construction == Some(true) {
2239 arguments.push(ast::LabeledArg {
2240 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2241 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2242 value: ast::LiteralValue::Bool(true),
2243 raw: "true".to_string(),
2244 digest: None,
2245 }))),
2246 });
2247 }
2248 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2249 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2250 unlabeled: None,
2251 arguments,
2252 digest: None,
2253 non_code_meta: Default::default(),
2254 })));
2255
2256 let sketch_id = sketch;
2258 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2259 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2260 })?;
2261 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2262 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2263 "Object is not a sketch, it is {}",
2264 sketch_object.kind.human_friendly_kind_with_article(),
2265 ))));
2266 };
2267 let mut new_ast = self.program.ast.clone();
2269 let (sketch_block_ref, _) = self
2270 .mutate_ast(
2271 &mut new_ast,
2272 sketch_id,
2273 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2274 )
2275 .map_err(KclErrorWithOutputs::no_outputs)?;
2276 let new_source = source_from_ast(&new_ast);
2278 let (new_program, errors) = Program::parse(&new_source)
2280 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2281 if !errors.is_empty() {
2282 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2283 "Error parsing KCL source after adding arc: {errors:?}"
2284 ))));
2285 }
2286 let Some(new_program) = new_program else {
2287 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2288 "No AST produced after adding arc".to_string(),
2289 )));
2290 };
2291
2292 let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2293 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2294 "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2295 )))
2296 })?;
2297
2298 self.program = new_program.clone();
2300
2301 let mut truncated_program = new_program;
2303 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2304 .map_err(KclErrorWithOutputs::no_outputs)?;
2305
2306 let outcome = ctx
2308 .run_mock(
2309 &truncated_program,
2310 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2311 )
2312 .await?;
2313
2314 let new_object_ids = {
2315 let make_err =
2316 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2317 let segment_id = outcome
2318 .source_range_to_object
2319 .get(&arc_node_ref.range)
2320 .copied()
2321 .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2322 let segment_object = outcome
2323 .scene_objects
2324 .get(segment_id.0)
2325 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2326 let ObjectKind::Segment { segment } = &segment_object.kind else {
2327 return Err(make_err(format!(
2328 "Object is not a segment, it is {}",
2329 segment_object.kind.human_friendly_kind_with_article()
2330 )));
2331 };
2332 let Segment::Arc(arc) = segment else {
2333 return Err(make_err(format!(
2334 "Segment is not an arc, it is {}",
2335 segment.human_friendly_kind_with_article()
2336 )));
2337 };
2338 vec![arc.start, arc.end, arc.center, segment_id]
2339 };
2340 let src_delta = SourceDelta { text: new_source };
2341 let outcome = self.update_state_after_exec(outcome, false);
2343 let scene_graph_delta = SceneGraphDelta {
2344 new_graph: self.scene_graph_for_ui(),
2345 invalidates_ids: false,
2346 new_objects: new_object_ids,
2347 exec_outcome: outcome,
2348 };
2349 Ok((src_delta, scene_graph_delta))
2350 }
2351
2352 async fn add_circle(
2353 &mut self,
2354 ctx: &ExecutorContext,
2355 sketch: ObjectId,
2356 ctor: CircleCtor,
2357 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2358 let start_ast = to_ast_point2d(&ctor.start)
2360 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2361 let center_ast = to_ast_point2d(&ctor.center)
2362 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2363 let mut arguments = vec![
2364 ast::LabeledArg {
2365 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2366 arg: start_ast,
2367 },
2368 ast::LabeledArg {
2369 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2370 arg: center_ast,
2371 },
2372 ];
2373 if ctor.construction == Some(true) {
2375 arguments.push(ast::LabeledArg {
2376 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2377 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2378 value: ast::LiteralValue::Bool(true),
2379 raw: "true".to_string(),
2380 digest: None,
2381 }))),
2382 });
2383 }
2384 let circle_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2385 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2386 unlabeled: None,
2387 arguments,
2388 digest: None,
2389 non_code_meta: Default::default(),
2390 })));
2391
2392 let sketch_id = sketch;
2394 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2395 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2396 })?;
2397 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2398 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2399 "Object is not a sketch, it is {}",
2400 sketch_object.kind.human_friendly_kind_with_article(),
2401 ))));
2402 };
2403 let mut new_ast = self.program.ast.clone();
2405 let (sketch_block_ref, _) = self
2406 .mutate_ast(
2407 &mut new_ast,
2408 sketch_id,
2409 AstMutateCommand::AddSketchBlockVarDecl {
2410 prefix: CIRCLE_VARIABLE.to_owned(),
2411 expr: circle_ast,
2412 },
2413 )
2414 .map_err(KclErrorWithOutputs::no_outputs)?;
2415 let new_source = source_from_ast(&new_ast);
2417 let (new_program, errors) = Program::parse(&new_source)
2419 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2420 if !errors.is_empty() {
2421 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2422 "Error parsing KCL source after adding circle: {errors:?}"
2423 ))));
2424 }
2425 let Some(new_program) = new_program else {
2426 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2427 "No AST produced after adding circle".to_string(),
2428 )));
2429 };
2430
2431 let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2432 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2433 "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2434 )))
2435 })?;
2436
2437 self.program = new_program.clone();
2439
2440 let mut truncated_program = new_program;
2442 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2443 .map_err(KclErrorWithOutputs::no_outputs)?;
2444
2445 let outcome = ctx
2447 .run_mock(
2448 &truncated_program,
2449 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2450 )
2451 .await?;
2452
2453 let new_object_ids = {
2454 let make_err =
2455 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2456 let segment_id = outcome
2457 .source_range_to_object
2458 .get(&circle_node_ref.range)
2459 .copied()
2460 .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2461 let segment_object = outcome
2462 .scene_objects
2463 .get(segment_id.0)
2464 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2465 let ObjectKind::Segment { segment } = &segment_object.kind else {
2466 return Err(make_err(format!(
2467 "Object is not a segment, it is {}",
2468 segment_object.kind.human_friendly_kind_with_article()
2469 )));
2470 };
2471 let Segment::Circle(circle) = segment else {
2472 return Err(make_err(format!(
2473 "Segment is not a circle, it is {}",
2474 segment.human_friendly_kind_with_article()
2475 )));
2476 };
2477 vec![circle.start, circle.center, segment_id]
2478 };
2479 let src_delta = SourceDelta { text: new_source };
2480 let outcome = self.update_state_after_exec(outcome, false);
2482 let scene_graph_delta = SceneGraphDelta {
2483 new_graph: self.scene_graph_for_ui(),
2484 invalidates_ids: false,
2485 new_objects: new_object_ids,
2486 exec_outcome: outcome,
2487 };
2488 Ok((src_delta, scene_graph_delta))
2489 }
2490
2491 async fn add_control_point_spline(
2492 &mut self,
2493 ctx: &ExecutorContext,
2494 sketch: ObjectId,
2495 ctor: ControlPointSplineCtor,
2496 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2497 let new_program = ensure_control_point_spline_experimental_features(&self.program)
2498 .map_err(KclErrorWithOutputs::no_outputs)?;
2499
2500 let points_ast = to_ast_point2d_array(&ctor.points)
2501 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2502 let mut arguments = vec![ast::LabeledArg {
2503 label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2504 arg: points_ast,
2505 }];
2506 if ctor.construction == Some(true) {
2507 arguments.push(ast::LabeledArg {
2508 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2509 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
2510 value: ast::LiteralValue::Bool(true),
2511 raw: "true".to_string(),
2512 digest: None,
2513 }))),
2514 });
2515 }
2516 let spline_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2517 callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2518 unlabeled: None,
2519 arguments,
2520 digest: None,
2521 non_code_meta: Default::default(),
2522 })));
2523
2524 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2525 KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2526 })?;
2527 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2528 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2529 "Object is not a sketch, it is {}",
2530 sketch_object.kind.human_friendly_kind_with_article(),
2531 ))));
2532 };
2533
2534 let mut new_ast = new_program.ast.clone();
2535 let (sketch_block_ref, _) = self
2536 .mutate_ast(
2537 &mut new_ast,
2538 sketch,
2539 AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2540 )
2541 .map_err(KclErrorWithOutputs::no_outputs)?;
2542 let new_source = source_from_ast(&new_ast);
2543 let (new_program, errors) = Program::parse(&new_source)
2544 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2545 if !errors.is_empty() {
2546 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2547 "Error parsing KCL source after adding controlPointSpline: {errors:?}"
2548 ))));
2549 }
2550 let Some(new_program) = new_program else {
2551 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
2552 "No AST produced after adding controlPointSpline".to_string(),
2553 )));
2554 };
2555
2556 let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2557 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2558 "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2559 )))
2560 })?;
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 let new_object_ids = {
2576 let make_err =
2577 |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2578 let segment_id = outcome
2579 .source_range_to_object
2580 .get(&spline_node_ref.range)
2581 .copied()
2582 .ok_or_else(|| {
2583 make_err(format!(
2584 "Source range of controlPointSpline not found: {spline_node_ref:?}"
2585 ))
2586 })?;
2587 let segment_object = outcome
2588 .scene_objects
2589 .get(segment_id.0)
2590 .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2591 let ObjectKind::Segment { segment } = &segment_object.kind else {
2592 return Err(make_err(format!(
2593 "Object is not a segment, it is {}",
2594 segment_object.kind.human_friendly_kind_with_article()
2595 )));
2596 };
2597 let Segment::ControlPointSpline(spline) = segment else {
2598 return Err(make_err(format!(
2599 "Segment is not a control point spline, it is {}",
2600 segment.human_friendly_kind_with_article()
2601 )));
2602 };
2603
2604 let mut ids = outcome
2605 .scene_objects
2606 .iter()
2607 .filter_map(|obj| match &obj.kind {
2608 ObjectKind::Segment {
2609 segment: Segment::Line(line),
2610 } if line.owner == Some(segment_id) => Some(obj.id),
2611 _ => None,
2612 })
2613 .collect::<Vec<_>>();
2614 ids.extend(spline.controls.clone());
2615 ids.push(segment_id);
2616 ids
2617 };
2618 let src_delta = SourceDelta { text: new_source };
2619 let outcome = self.update_state_after_exec(outcome, false);
2620 let scene_graph_delta = SceneGraphDelta {
2621 new_graph: self.scene_graph_for_ui(),
2622 invalidates_ids: false,
2623 new_objects: new_object_ids,
2624 exec_outcome: outcome,
2625 };
2626 Ok((src_delta, scene_graph_delta))
2627 }
2628
2629 fn edit_point(
2630 &mut self,
2631 new_ast: &mut ast::Node<ast::Program>,
2632 sketch: ObjectId,
2633 point: ObjectId,
2634 ctor: PointCtor,
2635 ) -> Result<(), KclError> {
2636 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2638
2639 let sketch_id = sketch;
2641 let sketch_object = self
2642 .scene_graph
2643 .objects
2644 .get(sketch_id.0)
2645 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2646 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2647 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2648 };
2649 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2650 KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2651 })?;
2652 let point_id = point;
2654 let point_object = self
2655 .scene_graph
2656 .objects
2657 .get(point_id.0)
2658 .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2659 let ObjectKind::Segment {
2660 segment: Segment::Point(point),
2661 } = &point_object.kind
2662 else {
2663 return Err(KclError::refactor(format!(
2664 "Object is not a point segment: {point_object:?}"
2665 )));
2666 };
2667
2668 if let Some(owner_id) = point.owner {
2670 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2671 KclError::refactor(format!(
2672 "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2673 ))
2674 })?;
2675 let ObjectKind::Segment { segment } = &owner_object.kind else {
2676 return Err(KclError::refactor(format!(
2677 "Internal: Owner of point is not a segment, but found {}",
2678 owner_object.kind.human_friendly_kind_with_article()
2679 )));
2680 };
2681
2682 if let Segment::Line(line) = segment {
2684 let SegmentCtor::Line(line_ctor) = &line.ctor else {
2685 return Err(KclError::refactor(format!(
2686 "Internal: Owner of point does not have line ctor, but found {}",
2687 line.ctor.human_friendly_kind_with_article()
2688 )));
2689 };
2690 let mut line_ctor = line_ctor.clone();
2691 if line.start == point_id {
2693 line_ctor.start = ctor.position;
2694 } else if line.end == point_id {
2695 line_ctor.end = ctor.position;
2696 } else {
2697 return Err(KclError::refactor(format!(
2698 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2699 )));
2700 }
2701 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2702 }
2703
2704 if let Segment::Arc(arc) = segment {
2706 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2707 return Err(KclError::refactor(format!(
2708 "Internal: Owner of point does not have arc ctor, but found {}",
2709 arc.ctor.human_friendly_kind_with_article()
2710 )));
2711 };
2712 let mut arc_ctor = arc_ctor.clone();
2713 if arc.center == point_id {
2715 arc_ctor.center = ctor.position;
2716 } else if arc.start == point_id {
2717 arc_ctor.start = ctor.position;
2718 } else if arc.end == point_id {
2719 arc_ctor.end = ctor.position;
2720 } else {
2721 return Err(KclError::refactor(format!(
2722 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2723 )));
2724 }
2725 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2726 }
2727
2728 if let Segment::Circle(circle) = segment {
2730 let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2731 return Err(KclError::refactor(format!(
2732 "Internal: Owner of point does not have circle ctor, but found {}",
2733 circle.ctor.human_friendly_kind_with_article()
2734 )));
2735 };
2736 let mut circle_ctor = circle_ctor.clone();
2737 if circle.center == point_id {
2738 circle_ctor.center = ctor.position;
2739 } else if circle.start == point_id {
2740 circle_ctor.start = ctor.position;
2741 } else {
2742 return Err(KclError::refactor(format!(
2743 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2744 )));
2745 }
2746 return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2747 }
2748
2749 if let Segment::ControlPointSpline(spline) = segment {
2750 let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2751 return Err(KclError::refactor(format!(
2752 "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2753 spline.ctor.human_friendly_kind_with_article()
2754 )));
2755 };
2756 let mut spline_ctor = spline_ctor.clone();
2757 let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2758 return Err(KclError::refactor(format!(
2759 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2760 )));
2761 };
2762 spline_ctor.points[control_index] = ctor.position;
2763 return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2764 }
2765
2766 }
2769
2770 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2772 Ok(())
2773 }
2774
2775 fn edit_line(
2776 &mut self,
2777 new_ast: &mut ast::Node<ast::Program>,
2778 sketch: ObjectId,
2779 line: ObjectId,
2780 ctor: LineCtor,
2781 ) -> Result<(), KclError> {
2782 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2784 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2785
2786 let sketch_id = sketch;
2788 let sketch_object = self
2789 .scene_graph
2790 .objects
2791 .get(sketch_id.0)
2792 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2793 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2794 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2795 };
2796 sketch
2797 .segments
2798 .iter()
2799 .find(|o| **o == line)
2800 .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2801 let line_id = line;
2803 let line_object = self
2804 .scene_graph
2805 .objects
2806 .get(line_id.0)
2807 .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2808 let ObjectKind::Segment { .. } = &line_object.kind else {
2809 let kind = line_object.kind.human_friendly_kind_with_article();
2810 return Err(KclError::refactor(format!(
2811 "This constraint only works on Segments, but you selected {kind}"
2812 )));
2813 };
2814
2815 self.mutate_ast(
2817 new_ast,
2818 line_id,
2819 AstMutateCommand::EditLine {
2820 start: new_start_ast,
2821 end: new_end_ast,
2822 construction: ctor.construction,
2823 },
2824 )?;
2825 Ok(())
2826 }
2827
2828 fn edit_arc(
2829 &mut self,
2830 new_ast: &mut ast::Node<ast::Program>,
2831 sketch: ObjectId,
2832 arc: ObjectId,
2833 ctor: ArcCtor,
2834 ) -> Result<(), KclError> {
2835 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2837 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2838 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2839
2840 let sketch_id = sketch;
2842 let sketch_object = self
2843 .scene_graph
2844 .objects
2845 .get(sketch_id.0)
2846 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2847 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2848 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2849 };
2850 sketch
2851 .segments
2852 .iter()
2853 .find(|o| **o == arc)
2854 .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2855 let arc_id = arc;
2857 let arc_object = self
2858 .scene_graph
2859 .objects
2860 .get(arc_id.0)
2861 .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2862 let ObjectKind::Segment { .. } = &arc_object.kind else {
2863 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2864 };
2865
2866 self.mutate_ast(
2868 new_ast,
2869 arc_id,
2870 AstMutateCommand::EditArc {
2871 start: new_start_ast,
2872 end: new_end_ast,
2873 center: new_center_ast,
2874 construction: ctor.construction,
2875 },
2876 )?;
2877 Ok(())
2878 }
2879
2880 fn edit_circle(
2881 &mut self,
2882 new_ast: &mut ast::Node<ast::Program>,
2883 sketch: ObjectId,
2884 circle: ObjectId,
2885 ctor: CircleCtor,
2886 ) -> Result<(), KclError> {
2887 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2889 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2890
2891 let sketch_id = sketch;
2893 let sketch_object = self
2894 .scene_graph
2895 .objects
2896 .get(sketch_id.0)
2897 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2898 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2899 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2900 };
2901 sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
2902 KclError::refactor(format!(
2903 "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
2904 ))
2905 })?;
2906 let circle_id = circle;
2908 let circle_object = self
2909 .scene_graph
2910 .objects
2911 .get(circle_id.0)
2912 .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
2913 let ObjectKind::Segment { .. } = &circle_object.kind else {
2914 return Err(KclError::refactor(format!(
2915 "Object is not a segment: {circle_object:?}"
2916 )));
2917 };
2918
2919 self.mutate_ast(
2921 new_ast,
2922 circle_id,
2923 AstMutateCommand::EditCircle {
2924 start: new_start_ast,
2925 center: new_center_ast,
2926 construction: ctor.construction,
2927 },
2928 )?;
2929 Ok(())
2930 }
2931
2932 fn edit_control_point_spline(
2933 &mut self,
2934 new_ast: &mut ast::Node<ast::Program>,
2935 sketch: ObjectId,
2936 spline: ObjectId,
2937 ctor: ControlPointSplineCtor,
2938 ) -> Result<(), KclError> {
2939 let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
2940
2941 let sketch_object = self
2942 .scene_graph
2943 .objects
2944 .get(sketch.0)
2945 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2946 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2947 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2948 };
2949 sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
2950 KclError::refactor(format!(
2951 "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
2952 ))
2953 })?;
2954
2955 let spline_object =
2956 self.scene_graph.objects.get(spline.0).ok_or_else(|| {
2957 KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
2958 })?;
2959 let ObjectKind::Segment { .. } = &spline_object.kind else {
2960 return Err(KclError::refactor(format!(
2961 "Object is not a segment: {spline_object:?}"
2962 )));
2963 };
2964
2965 self.mutate_ast(
2966 new_ast,
2967 spline,
2968 AstMutateCommand::EditControlPointSpline {
2969 points: points_ast,
2970 construction: ctor.construction,
2971 },
2972 )?;
2973 Ok(())
2974 }
2975
2976 fn delete_segment(
2977 &mut self,
2978 new_ast: &mut ast::Node<ast::Program>,
2979 sketch: ObjectId,
2980 segment_id: ObjectId,
2981 ) -> Result<(), KclError> {
2982 let sketch_id = sketch;
2984 let sketch_object = self
2985 .scene_graph
2986 .objects
2987 .get(sketch_id.0)
2988 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2989 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2990 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2991 };
2992 sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
2993 KclError::refactor(format!(
2994 "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
2995 ))
2996 })?;
2997 let segment_object =
2999 self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3000 KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3001 })?;
3002 let ObjectKind::Segment { .. } = &segment_object.kind else {
3003 return Err(KclError::refactor(format!(
3004 "Object is not a segment, it is {}",
3005 segment_object.kind.human_friendly_kind_with_article()
3006 )));
3007 };
3008
3009 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3011 Ok(())
3012 }
3013
3014 fn delete_constraint(
3015 &mut self,
3016 new_ast: &mut ast::Node<ast::Program>,
3017 sketch: ObjectId,
3018 constraint_id: ObjectId,
3019 ) -> Result<(), KclError> {
3020 let sketch_id = sketch;
3022 let sketch_object = self
3023 .scene_graph
3024 .objects
3025 .get(sketch_id.0)
3026 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3027 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3028 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3029 };
3030 sketch
3031 .constraints
3032 .iter()
3033 .find(|o| **o == constraint_id)
3034 .ok_or_else(|| {
3035 KclError::refactor(format!(
3036 "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3037 ))
3038 })?;
3039 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3041 KclError::refactor(format!(
3042 "Constraint not found in scene graph: constraint={constraint_id:?}"
3043 ))
3044 })?;
3045 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3046 return Err(KclError::refactor(format!(
3047 "Object is not a constraint, it is {}",
3048 constraint_object.kind.human_friendly_kind_with_article()
3049 )));
3050 };
3051
3052 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3054 Ok(())
3055 }
3056
3057 fn edit_coincident_constraint(
3058 &mut self,
3059 new_ast: &mut ast::Node<ast::Program>,
3060 constraint_id: ObjectId,
3061 segments: Vec<ConstraintSegment>,
3062 ) -> Result<(), KclError> {
3063 if segments.len() < 2 {
3064 return Err(KclError::refactor(format!(
3065 "Coincident constraint must have at least 2 inputs, got {}",
3066 segments.len()
3067 )));
3068 }
3069
3070 let segment_asts = segments
3071 .iter()
3072 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3073 .collect::<Result<Vec<_>, _>>()?;
3074
3075 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3076 elements: segment_asts,
3077 digest: None,
3078 non_code_meta: Default::default(),
3079 })));
3080
3081 self.mutate_ast(
3082 new_ast,
3083 constraint_id,
3084 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3085 )?;
3086 Ok(())
3087 }
3088
3089 fn edit_horizontal_points_constraint(
3090 &mut self,
3091 new_ast: &mut ast::Node<ast::Program>,
3092 constraint_id: ObjectId,
3093 points: Vec<ConstraintSegment>,
3094 ) -> Result<(), KclError> {
3095 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3096 }
3097
3098 fn edit_vertical_points_constraint(
3099 &mut self,
3100 new_ast: &mut ast::Node<ast::Program>,
3101 constraint_id: ObjectId,
3102 points: Vec<ConstraintSegment>,
3103 ) -> Result<(), KclError> {
3104 self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3105 }
3106
3107 fn edit_axis_points_constraint(
3108 &mut self,
3109 new_ast: &mut ast::Node<ast::Program>,
3110 constraint_id: ObjectId,
3111 points: Vec<ConstraintSegment>,
3112 constraint_name: &str,
3113 ) -> Result<(), KclError> {
3114 if points.len() < 2 {
3115 return Err(KclError::refactor(format!(
3116 "{constraint_name} points constraint must have at least 2 points, got {}",
3117 points.len()
3118 )));
3119 }
3120
3121 let point_asts = points
3122 .iter()
3123 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3124 .collect::<Result<Vec<_>, _>>()?;
3125
3126 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3127 elements: point_asts,
3128 digest: None,
3129 non_code_meta: Default::default(),
3130 })));
3131
3132 self.mutate_ast(
3133 new_ast,
3134 constraint_id,
3135 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3136 )?;
3137 Ok(())
3138 }
3139
3140 fn edit_equal_length_constraint(
3142 &mut self,
3143 new_ast: &mut ast::Node<ast::Program>,
3144 constraint_id: ObjectId,
3145 lines: Vec<ObjectId>,
3146 ) -> Result<(), KclError> {
3147 if lines.len() < 2 {
3148 return Err(KclError::refactor(format!(
3149 "Lines equal length constraint must have at least 2 lines, got {}",
3150 lines.len()
3151 )));
3152 }
3153
3154 let line_asts = lines
3155 .iter()
3156 .map(|line_id| {
3157 let line_object = self
3158 .scene_graph
3159 .objects
3160 .get(line_id.0)
3161 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3162 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3163 let kind = line_object.kind.human_friendly_kind_with_article();
3164 return Err(KclError::refactor(format!(
3165 "This constraint only works on Segments, but you selected {kind}"
3166 )));
3167 };
3168 let Segment::Line(_) = line_segment else {
3169 let kind = line_segment.human_friendly_kind_with_article();
3170 return Err(KclError::refactor(format!(
3171 "Only lines can be made equal length, but you selected {kind}"
3172 )));
3173 };
3174
3175 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3176 })
3177 .collect::<Result<Vec<_>, _>>()?;
3178
3179 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3180 elements: line_asts,
3181 digest: None,
3182 non_code_meta: Default::default(),
3183 })));
3184
3185 self.mutate_ast(
3186 new_ast,
3187 constraint_id,
3188 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3189 )?;
3190 Ok(())
3191 }
3192
3193 fn edit_parallel_constraint(
3195 &mut self,
3196 new_ast: &mut ast::Node<ast::Program>,
3197 constraint_id: ObjectId,
3198 lines: Vec<ObjectId>,
3199 ) -> Result<(), KclError> {
3200 if lines.len() < 2 {
3201 return Err(KclError::refactor(format!(
3202 "Parallel constraint must have at least 2 lines, got {}",
3203 lines.len()
3204 )));
3205 }
3206
3207 let line_asts = lines
3208 .iter()
3209 .map(|line_id| {
3210 let line_object = self
3211 .scene_graph
3212 .objects
3213 .get(line_id.0)
3214 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3215 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3216 let kind = line_object.kind.human_friendly_kind_with_article();
3217 return Err(KclError::refactor(format!(
3218 "This constraint only works on Segments, but you selected {kind}"
3219 )));
3220 };
3221 let Segment::Line(_) = line_segment else {
3222 let kind = line_segment.human_friendly_kind_with_article();
3223 return Err(KclError::refactor(format!(
3224 "Only lines can be made parallel, but you selected {kind}"
3225 )));
3226 };
3227
3228 get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3229 })
3230 .collect::<Result<Vec<_>, _>>()?;
3231
3232 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3233 elements: line_asts,
3234 digest: None,
3235 non_code_meta: Default::default(),
3236 })));
3237
3238 self.mutate_ast(
3239 new_ast,
3240 constraint_id,
3241 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3242 )?;
3243 Ok(())
3244 }
3245
3246 fn edit_equal_radius_constraint(
3248 &mut self,
3249 new_ast: &mut ast::Node<ast::Program>,
3250 constraint_id: ObjectId,
3251 input: Vec<ObjectId>,
3252 ) -> Result<(), KclError> {
3253 if input.len() < 2 {
3254 return Err(KclError::refactor(format!(
3255 "equalRadius constraint must have at least 2 segments, got {}",
3256 input.len()
3257 )));
3258 }
3259
3260 let input_asts = input
3261 .iter()
3262 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3263 .collect::<Result<Vec<_>, _>>()?;
3264
3265 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3266 elements: input_asts,
3267 digest: None,
3268 non_code_meta: Default::default(),
3269 })));
3270
3271 self.mutate_ast(
3272 new_ast,
3273 constraint_id,
3274 AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3275 )?;
3276 Ok(())
3277 }
3278
3279 async fn execute_after_edit(
3280 &mut self,
3281 ctx: &ExecutorContext,
3282 sketch: ObjectId,
3283 sketch_block_ref: AstNodeRef,
3284 new_ast: &mut ast::Node<ast::Program>,
3285 options: ExecuteAfterEditOptions,
3286 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3287 let ExecuteAfterEditOptions {
3288 segment_ids_edited,
3289 edit_kind,
3290 commit_solved_initial_guesses,
3291 } = options;
3292
3293 let new_source = source_from_ast(new_ast);
3295 let (new_program, errors) = Program::parse(&new_source)
3297 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3298 if !errors.is_empty() {
3299 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3300 "Error parsing KCL source after editing: {errors:?}"
3301 ))));
3302 }
3303 let Some(new_program) = new_program else {
3304 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3305 "No AST produced after editing".to_string(),
3306 )));
3307 };
3308
3309 self.program = new_program.clone();
3311
3312 let is_delete = edit_kind.is_delete();
3314 let truncated_program = {
3315 let mut truncated_program = new_program;
3316 only_sketch_block(
3317 &mut truncated_program.ast,
3318 &sketch_block_ref,
3319 edit_kind.to_change_kind(),
3320 )
3321 .map_err(KclErrorWithOutputs::no_outputs)?;
3322 truncated_program
3323 };
3324
3325 let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3327 let mock_config = MockConfig {
3328 sketch_block_id: Some(sketch),
3329 freedom_analysis: is_delete,
3330 segment_ids_edited: segment_ids_edited.clone(),
3331 drag_anchors,
3332 ..Default::default()
3333 };
3334 let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3335
3336 let outcome = self.update_state_after_exec(outcome, is_delete);
3338
3339 let src_delta = if commit_solved_initial_guesses {
3340 self.commit_var_solutions_to_program(&outcome, "editing")?
3341 } else {
3342 SourceDelta { text: new_source }
3343 };
3344 let scene_graph_delta = SceneGraphDelta {
3345 new_graph: self.scene_graph_for_ui(),
3346 invalidates_ids: is_delete,
3347 new_objects: Vec::new(),
3348 exec_outcome: outcome,
3349 };
3350 Ok((src_delta, scene_graph_delta))
3351 }
3352
3353 async fn execute_after_delete_sketch(
3354 &mut self,
3355 ctx: &ExecutorContext,
3356 new_ast: &mut ast::Node<ast::Program>,
3357 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3358 let new_source = source_from_ast(new_ast);
3360 let (new_program, errors) = Program::parse(&new_source)
3362 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
3363 if !errors.is_empty() {
3364 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
3365 "Error parsing KCL source after editing: {errors:?}"
3366 ))));
3367 }
3368 let Some(new_program) = new_program else {
3369 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
3370 "No AST produced after editing".to_string(),
3371 )));
3372 };
3373
3374 self.program = new_program.clone();
3376
3377 let outcome = ctx.run_with_caching(new_program).await?;
3383 let freedom_analysis_ran = true;
3384
3385 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3386
3387 let src_delta = SourceDelta { text: new_source };
3388 let scene_graph_delta = SceneGraphDelta {
3389 new_graph: self.scene_graph_for_ui(),
3390 invalidates_ids: true,
3391 new_objects: Vec::new(),
3392 exec_outcome: outcome,
3393 };
3394 Ok((src_delta, scene_graph_delta))
3395 }
3396
3397 fn point_id_to_ast_reference(
3402 &self,
3403 point_id: ObjectId,
3404 new_ast: &mut ast::Node<ast::Program>,
3405 ) -> Result<ast::Expr, KclError> {
3406 let point_object = self
3407 .scene_graph
3408 .objects
3409 .get(point_id.0)
3410 .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3411 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3412 return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3413 };
3414 let Segment::Point(point) = point_segment else {
3415 return Err(KclError::refactor(format!(
3416 "Only points are currently supported: {point_object:?}"
3417 )));
3418 };
3419
3420 if let Some(owner_id) = point.owner {
3421 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3422 KclError::refactor(format!(
3423 "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3424 ))
3425 })?;
3426 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3427 return Err(KclError::refactor(format!(
3428 "Owner of point is not a segment, but found {}",
3429 owner_object.kind.human_friendly_kind_with_article()
3430 )));
3431 };
3432
3433 match owner_segment {
3434 Segment::Line(line) => {
3435 let property = if line.start == point_id {
3436 LINE_PROPERTY_START
3437 } else if line.end == point_id {
3438 LINE_PROPERTY_END
3439 } else {
3440 return Err(KclError::refactor(format!(
3441 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3442 )));
3443 };
3444 get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3445 }
3446 Segment::Arc(arc) => {
3447 let property = if arc.start == point_id {
3448 ARC_PROPERTY_START
3449 } else if arc.end == point_id {
3450 ARC_PROPERTY_END
3451 } else if arc.center == point_id {
3452 ARC_PROPERTY_CENTER
3453 } else {
3454 return Err(KclError::refactor(format!(
3455 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3456 )));
3457 };
3458 get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3459 }
3460 Segment::Circle(circle) => {
3461 let property = if circle.start == point_id {
3462 CIRCLE_PROPERTY_START
3463 } else if circle.center == point_id {
3464 CIRCLE_PROPERTY_CENTER
3465 } else {
3466 return Err(KclError::refactor(format!(
3467 "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3468 )));
3469 };
3470 get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3471 }
3472 Segment::ControlPointSpline(spline) => {
3473 let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3474 return Err(KclError::refactor(format!(
3475 "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3476 )));
3477 };
3478 let owner_expr =
3479 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3480 let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3481 Ok(create_index_expression(controls_expr, index))
3482 }
3483 _ => Err(KclError::refactor(format!(
3484 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3485 ))),
3486 }
3487 } else {
3488 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3490 }
3491 }
3492
3493 fn line_id_to_ast_reference(
3494 &self,
3495 line_id: ObjectId,
3496 new_ast: &mut ast::Node<ast::Program>,
3497 ) -> Result<ast::Expr, KclError> {
3498 let line_object = self
3499 .scene_graph
3500 .objects
3501 .get(line_id.0)
3502 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3503 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3504 return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3505 };
3506 let Segment::Line(line) = line_segment else {
3507 return Err(KclError::refactor(format!(
3508 "Only lines are currently supported: {line_object:?}"
3509 )));
3510 };
3511
3512 if let Some(owner_id) = line.owner {
3513 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3514 KclError::refactor(format!(
3515 "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3516 ))
3517 })?;
3518 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3519 return Err(KclError::refactor(format!(
3520 "Owner of line is not a segment, but found {}",
3521 owner_object.kind.human_friendly_kind_with_article()
3522 )));
3523 };
3524
3525 match owner_segment {
3526 Segment::ControlPointSpline(spline) => {
3527 let Some(index) = spline
3528 .controls
3529 .windows(2)
3530 .position(|window| window[0] == line.start && window[1] == line.end)
3531 else {
3532 return Err(KclError::refactor(format!(
3533 "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3534 )));
3535 };
3536 let owner_expr =
3537 get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3538 let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3539 Ok(create_index_expression(edges_expr, index))
3540 }
3541 _ => Err(KclError::refactor(format!(
3542 "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3543 ))),
3544 }
3545 } else {
3546 get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3547 }
3548 }
3549
3550 fn coincident_segment_to_ast(
3551 &self,
3552 segment: &ConstraintSegment,
3553 new_ast: &mut ast::Node<ast::Program>,
3554 ) -> Result<ast::Expr, KclError> {
3555 match segment {
3556 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3557 ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3558 }
3559 }
3560
3561 fn segment_id_to_constraint_ast_reference(
3562 &self,
3563 segment_id: ObjectId,
3564 new_ast: &mut ast::Node<ast::Program>,
3565 ) -> Result<ast::Expr, KclError> {
3566 let segment_object = self
3567 .scene_graph
3568 .objects
3569 .get(segment_id.0)
3570 .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3571 let ObjectKind::Segment { segment } = &segment_object.kind else {
3572 return Err(KclError::refactor(format!(
3573 "Object is not a segment, it is {}",
3574 segment_object.kind.human_friendly_kind_with_article()
3575 )));
3576 };
3577
3578 match segment {
3579 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3580 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3581 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3582 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3583 Segment::ControlPointSpline(_) => {
3584 get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3585 }
3586 }
3587 }
3588
3589 fn axis_constraint_segment_to_ast(
3590 &self,
3591 segment: &ConstraintSegment,
3592 new_ast: &mut ast::Node<ast::Program>,
3593 ) -> Result<ast::Expr, KclError> {
3594 match segment {
3595 ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3596 ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3597 }
3598 }
3599
3600 async fn add_coincident(
3601 &mut self,
3602 sketch: ObjectId,
3603 coincident: Coincident,
3604 new_ast: &mut ast::Node<ast::Program>,
3605 ) -> Result<AstNodeRef, KclError> {
3606 let sketch_id = sketch;
3607 for segment in &coincident.segments {
3608 let ConstraintSegment::Segment(segment_id) = segment else {
3609 continue;
3610 };
3611 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3612 continue;
3613 };
3614 if matches!(
3615 segment_object.kind,
3616 ObjectKind::Segment {
3617 segment: Segment::ControlPointSpline(_)
3618 }
3619 ) {
3620 return Err(KclError::refactor(
3621 "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3622 .to_owned(),
3623 ));
3624 }
3625 }
3626 let segment_asts = coincident
3627 .segments
3628 .iter()
3629 .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3630 .collect::<Result<Vec<_>, _>>()?;
3631 if segment_asts.len() < 2 {
3632 return Err(KclError::refactor(format!(
3633 "Coincident constraint must have at least 2 inputs, got {}",
3634 segment_asts.len()
3635 )));
3636 }
3637
3638 let coincident_ast = create_coincident_ast(segment_asts);
3640
3641 let (sketch_block_ref, _) = self.mutate_ast(
3643 new_ast,
3644 sketch_id,
3645 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3646 )?;
3647 Ok(sketch_block_ref)
3648 }
3649
3650 async fn add_distance(
3651 &mut self,
3652 sketch: ObjectId,
3653 distance: Distance,
3654 new_ast: &mut ast::Node<ast::Program>,
3655 ) -> Result<AstNodeRef, KclError> {
3656 let sketch_id = sketch;
3657 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
3658 [pt0, pt1] => [
3659 self.coincident_segment_to_ast(pt0, new_ast)?,
3660 self.coincident_segment_to_ast(pt1, new_ast)?,
3661 ],
3662 _ => {
3663 return Err(KclError::refactor(format!(
3664 "Distance constraint must have exactly 2 points, got {}",
3665 distance.points.len()
3666 )));
3667 }
3668 };
3669
3670 let arguments = match &distance.label_position {
3671 Some(label_position) => vec![ast::LabeledArg {
3672 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3673 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3674 }],
3675 None => Default::default(),
3676 };
3677
3678 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3680 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
3681 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3682 ast::ArrayExpression {
3683 elements: vec![pt0_ast, pt1_ast],
3684 digest: None,
3685 non_code_meta: Default::default(),
3686 },
3687 )))),
3688 arguments,
3689 digest: None,
3690 non_code_meta: Default::default(),
3691 })));
3692 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3693 left: distance_call_ast,
3694 operator: ast::BinaryOperator::Eq,
3695 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3696 value: ast::LiteralValue::Number {
3697 value: distance.distance.value,
3698 suffix: distance.distance.units,
3699 },
3700 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3701 KclError::refactor(format!(
3702 "Could not format numeric suffix: {:?}",
3703 distance.distance.units
3704 ))
3705 })?,
3706 digest: None,
3707 }))),
3708 digest: None,
3709 })));
3710
3711 let (sketch_block_ref, _) = self.mutate_ast(
3713 new_ast,
3714 sketch_id,
3715 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3716 )?;
3717 Ok(sketch_block_ref)
3718 }
3719
3720 async fn add_angle(
3721 &mut self,
3722 sketch: ObjectId,
3723 angle: Angle,
3724 new_ast: &mut ast::Node<ast::Program>,
3725 ) -> Result<AstNodeRef, KclError> {
3726 let &[l0_id, l1_id] = angle.lines.as_slice() else {
3727 return Err(KclError::refactor(format!(
3728 "Angle constraint must have exactly 2 lines, got {}",
3729 angle.lines.len()
3730 )));
3731 };
3732 let sketch_id = sketch;
3733
3734 let line0_object = self
3736 .scene_graph
3737 .objects
3738 .get(l0_id.0)
3739 .ok_or_else(|| KclError::refactor(format!("Line not found: {l0_id:?}")))?;
3740 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
3741 return Err(KclError::refactor(format!("Object is not a segment: {line0_object:?}")));
3742 };
3743 let Segment::Line(_) = line0_segment else {
3744 return Err(KclError::refactor(format!(
3745 "Only lines can be constrained to meet at an angle: {line0_object:?}",
3746 )));
3747 };
3748 let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3749
3750 let line1_object = self
3751 .scene_graph
3752 .objects
3753 .get(l1_id.0)
3754 .ok_or_else(|| KclError::refactor(format!("Line not found: {l1_id:?}")))?;
3755 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
3756 return Err(KclError::refactor(format!("Object is not a segment: {line1_object:?}")));
3757 };
3758 let Segment::Line(_) = line1_segment else {
3759 return Err(KclError::refactor(format!(
3760 "Only lines can be constrained to meet at an angle: {line1_object:?}",
3761 )));
3762 };
3763 let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3764
3765 let angle_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3767 callee: ast::Node::no_src(ast_sketch2_name(ANGLE_FN)),
3768 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
3769 ast::ArrayExpression {
3770 elements: vec![l0_ast, l1_ast],
3771 digest: None,
3772 non_code_meta: Default::default(),
3773 },
3774 )))),
3775 arguments: Default::default(),
3776 digest: None,
3777 non_code_meta: Default::default(),
3778 })));
3779 let angle_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
3780 left: angle_call_ast,
3781 operator: ast::BinaryOperator::Eq,
3782 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
3783 value: ast::LiteralValue::Number {
3784 value: angle.angle.value,
3785 suffix: angle.angle.units,
3786 },
3787 raw: format_number_literal(angle.angle.value, angle.angle.units, None).map_err(|_| {
3788 KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units))
3789 })?,
3790 digest: None,
3791 }))),
3792 digest: None,
3793 })));
3794
3795 let (sketch_block_ref, _) = self.mutate_ast(
3797 new_ast,
3798 sketch_id,
3799 AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3800 )?;
3801 Ok(sketch_block_ref)
3802 }
3803
3804 async fn add_tangent(
3805 &mut self,
3806 sketch: ObjectId,
3807 tangent: Tangent,
3808 new_ast: &mut ast::Node<ast::Program>,
3809 ) -> Result<AstNodeRef, KclError> {
3810 let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3811 return Err(KclError::refactor(format!(
3812 "Tangent constraint must have exactly 2 segments, got {}",
3813 tangent.input.len()
3814 )));
3815 };
3816 let sketch_id = sketch;
3817
3818 let seg0_object = self
3819 .scene_graph
3820 .objects
3821 .get(seg0_id.0)
3822 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3823 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3824 return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3825 };
3826 let seg0_ast = match seg0_segment {
3827 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3828 self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3829 }
3830 _ => {
3831 return Err(KclError::refactor(format!(
3832 "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3833 )));
3834 }
3835 };
3836
3837 let seg1_object = self
3838 .scene_graph
3839 .objects
3840 .get(seg1_id.0)
3841 .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3842 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3843 return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3844 };
3845 let seg1_ast = match seg1_segment {
3846 Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3847 self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3848 }
3849 _ => {
3850 return Err(KclError::refactor(format!(
3851 "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
3852 )));
3853 }
3854 };
3855
3856 let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
3857 let (sketch_block_ref, _) = self.mutate_ast(
3858 new_ast,
3859 sketch_id,
3860 AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
3861 )?;
3862 Ok(sketch_block_ref)
3863 }
3864
3865 async fn add_symmetric(
3866 &mut self,
3867 sketch: ObjectId,
3868 symmetric: Symmetric,
3869 new_ast: &mut ast::Node<ast::Program>,
3870 ) -> Result<AstNodeRef, KclError> {
3871 let &[input0_id, input1_id] = symmetric.input.as_slice() else {
3872 return Err(KclError::refactor(format!(
3873 "Symmetric constraint must have exactly 2 inputs, got {}",
3874 symmetric.input.len()
3875 )));
3876 };
3877 let sketch_id = sketch;
3878
3879 let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
3880 let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
3881 let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
3882
3883 let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
3884 let (sketch_block_ref, _) = self.mutate_ast(
3885 new_ast,
3886 sketch_id,
3887 AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
3888 )?;
3889 Ok(sketch_block_ref)
3890 }
3891
3892 async fn add_midpoint(
3893 &mut self,
3894 sketch: ObjectId,
3895 midpoint: Midpoint,
3896 new_ast: &mut ast::Node<ast::Program>,
3897 ) -> Result<AstNodeRef, KclError> {
3898 let sketch_id = sketch;
3899 let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
3900
3901 let segment_object = self
3902 .scene_graph
3903 .objects
3904 .get(midpoint.segment.0)
3905 .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
3906 let ObjectKind::Segment {
3907 segment: midpoint_segment,
3908 } = &segment_object.kind
3909 else {
3910 return Err(KclError::refactor(format!(
3911 "Object must be a segment, but it was {}",
3912 segment_object.kind.human_friendly_kind_with_article()
3913 )));
3914 };
3915 let segment_ast = match midpoint_segment {
3916 Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
3917 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
3918 _ => {
3919 return Err(KclError::refactor(format!(
3920 "Midpoint target must be a line or arc segment but it was {}",
3921 midpoint_segment.human_friendly_kind_with_article()
3922 )));
3923 }
3924 };
3925
3926 let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
3927 let (sketch_block_ref, _) = self.mutate_ast(
3928 new_ast,
3929 sketch_id,
3930 AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
3931 )?;
3932 Ok(sketch_block_ref)
3933 }
3934
3935 async fn add_equal_radius(
3936 &mut self,
3937 sketch: ObjectId,
3938 equal_radius: EqualRadius,
3939 new_ast: &mut ast::Node<ast::Program>,
3940 ) -> Result<AstNodeRef, KclError> {
3941 if equal_radius.input.len() < 2 {
3942 return Err(KclError::refactor(format!(
3943 "equalRadius constraint must have at least 2 segments, got {}",
3944 equal_radius.input.len()
3945 )));
3946 }
3947
3948 let sketch_id = sketch;
3949 let input_asts = equal_radius
3950 .input
3951 .iter()
3952 .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3953 .collect::<Result<Vec<_>, _>>()?;
3954
3955 let equal_radius_ast = create_equal_radius_ast(input_asts);
3956 let (sketch_block_ref, _) = self.mutate_ast(
3957 new_ast,
3958 sketch_id,
3959 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
3960 )?;
3961 Ok(sketch_block_ref)
3962 }
3963
3964 async fn add_radius(
3965 &mut self,
3966 sketch: ObjectId,
3967 radius: Radius,
3968 new_ast: &mut ast::Node<ast::Program>,
3969 ) -> Result<AstNodeRef, KclError> {
3970 let params = ArcSizeConstraintParams {
3971 points: vec![radius.arc],
3972 function_name: RADIUS_FN,
3973 value: radius.radius.value,
3974 units: radius.radius.units,
3975 label_position: radius.label_position,
3976 constraint_type_name: "Radius",
3977 };
3978 self.add_arc_size_constraint(sketch, params, new_ast).await
3979 }
3980
3981 async fn add_diameter(
3982 &mut self,
3983 sketch: ObjectId,
3984 diameter: Diameter,
3985 new_ast: &mut ast::Node<ast::Program>,
3986 ) -> Result<AstNodeRef, KclError> {
3987 let params = ArcSizeConstraintParams {
3988 points: vec![diameter.arc],
3989 function_name: DIAMETER_FN,
3990 value: diameter.diameter.value,
3991 units: diameter.diameter.units,
3992 label_position: diameter.label_position,
3993 constraint_type_name: "Diameter",
3994 };
3995 self.add_arc_size_constraint(sketch, params, new_ast).await
3996 }
3997
3998 async fn add_fixed_constraints(
3999 &mut self,
4000 sketch: ObjectId,
4001 points: Vec<FixedPoint>,
4002 new_ast: &mut ast::Node<ast::Program>,
4003 ) -> Result<AstNodeRef, KclError> {
4004 let mut sketch_block_ref = None;
4005
4006 for fixed_point in points {
4007 let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4008 let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4009 .map_err(|err| KclError::refactor(err.to_string()))?;
4010
4011 let (sketch_ref, _) = self.mutate_ast(
4012 new_ast,
4013 sketch,
4014 AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4015 )?;
4016 sketch_block_ref = Some(sketch_ref);
4017 }
4018
4019 sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4020 }
4021
4022 async fn add_arc_size_constraint(
4023 &mut self,
4024 sketch: ObjectId,
4025 params: ArcSizeConstraintParams,
4026 new_ast: &mut ast::Node<ast::Program>,
4027 ) -> Result<AstNodeRef, KclError> {
4028 let sketch_id = sketch;
4029
4030 if params.points.len() != 1 {
4032 return Err(KclError::refactor(format!(
4033 "{} constraint must have exactly 1 argument (an arc segment), got {}",
4034 params.constraint_type_name,
4035 params.points.len()
4036 )));
4037 }
4038
4039 let arc_id = params.points[0];
4040 let arc_object = self
4041 .scene_graph
4042 .objects
4043 .get(arc_id.0)
4044 .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4045 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4046 return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4047 };
4048 let ref_type = match arc_segment {
4049 Segment::Arc(_) => ARC_VARIABLE,
4050 Segment::Circle(_) => CIRCLE_VARIABLE,
4051 _ => {
4052 return Err(KclError::refactor(format!(
4053 "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4054 params.constraint_type_name
4055 )));
4056 }
4057 };
4058 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4060 let arguments = match ¶ms.label_position {
4061 Some(label_position) => vec![ast::LabeledArg {
4062 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4063 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4064 }],
4065 None => Default::default(),
4066 };
4067
4068 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4070 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4071 unlabeled: Some(arc_ast),
4072 arguments,
4073 digest: None,
4074 non_code_meta: Default::default(),
4075 })));
4076 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4077 left: call_ast,
4078 operator: ast::BinaryOperator::Eq,
4079 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4080 value: ast::LiteralValue::Number {
4081 value: params.value,
4082 suffix: params.units,
4083 },
4084 raw: format_number_literal(params.value, params.units, None)
4085 .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4086 digest: None,
4087 }))),
4088 digest: None,
4089 })));
4090
4091 let (sketch_block_ref, _) = self.mutate_ast(
4093 new_ast,
4094 sketch_id,
4095 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4096 )?;
4097 Ok(sketch_block_ref)
4098 }
4099
4100 async fn add_horizontal_distance(
4101 &mut self,
4102 sketch: ObjectId,
4103 distance: Distance,
4104 new_ast: &mut ast::Node<ast::Program>,
4105 ) -> Result<AstNodeRef, KclError> {
4106 let sketch_id = sketch;
4107 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4108 [pt0, pt1] => [
4109 self.coincident_segment_to_ast(pt0, new_ast)?,
4110 self.coincident_segment_to_ast(pt1, new_ast)?,
4111 ],
4112 _ => {
4113 return Err(KclError::refactor(format!(
4114 "Horizontal distance constraint must have exactly 2 points, got {}",
4115 distance.points.len()
4116 )));
4117 }
4118 };
4119
4120 let arguments = match &distance.label_position {
4121 Some(label_position) => vec![ast::LabeledArg {
4122 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4123 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4124 }],
4125 None => Default::default(),
4126 };
4127
4128 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4130 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
4131 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4132 ast::ArrayExpression {
4133 elements: vec![pt0_ast, pt1_ast],
4134 digest: None,
4135 non_code_meta: Default::default(),
4136 },
4137 )))),
4138 arguments,
4139 digest: None,
4140 non_code_meta: Default::default(),
4141 })));
4142 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4143 left: distance_call_ast,
4144 operator: ast::BinaryOperator::Eq,
4145 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4146 value: ast::LiteralValue::Number {
4147 value: distance.distance.value,
4148 suffix: distance.distance.units,
4149 },
4150 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4151 KclError::refactor(format!(
4152 "Could not format numeric suffix: {:?}",
4153 distance.distance.units
4154 ))
4155 })?,
4156 digest: None,
4157 }))),
4158 digest: None,
4159 })));
4160
4161 let (sketch_block_ref, _) = self.mutate_ast(
4163 new_ast,
4164 sketch_id,
4165 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4166 )?;
4167 Ok(sketch_block_ref)
4168 }
4169
4170 async fn add_vertical_distance(
4171 &mut self,
4172 sketch: ObjectId,
4173 distance: Distance,
4174 new_ast: &mut ast::Node<ast::Program>,
4175 ) -> Result<AstNodeRef, KclError> {
4176 let sketch_id = sketch;
4177 let [pt0_ast, pt1_ast] = match distance.points.as_slice() {
4178 [pt0, pt1] => [
4179 self.coincident_segment_to_ast(pt0, new_ast)?,
4180 self.coincident_segment_to_ast(pt1, new_ast)?,
4181 ],
4182 _ => {
4183 return Err(KclError::refactor(format!(
4184 "Vertical distance constraint must have exactly 2 points, got {}",
4185 distance.points.len()
4186 )));
4187 }
4188 };
4189
4190 let arguments = match &distance.label_position {
4191 Some(label_position) => vec![ast::LabeledArg {
4192 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4193 arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4194 }],
4195 None => Default::default(),
4196 };
4197
4198 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4200 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
4201 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4202 ast::ArrayExpression {
4203 elements: vec![pt0_ast, pt1_ast],
4204 digest: None,
4205 non_code_meta: Default::default(),
4206 },
4207 )))),
4208 arguments,
4209 digest: None,
4210 non_code_meta: Default::default(),
4211 })));
4212 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
4213 left: distance_call_ast,
4214 operator: ast::BinaryOperator::Eq,
4215 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
4216 value: ast::LiteralValue::Number {
4217 value: distance.distance.value,
4218 suffix: distance.distance.units,
4219 },
4220 raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
4221 KclError::refactor(format!(
4222 "Could not format numeric suffix: {:?}",
4223 distance.distance.units
4224 ))
4225 })?,
4226 digest: None,
4227 }))),
4228 digest: None,
4229 })));
4230
4231 let (sketch_block_ref, _) = self.mutate_ast(
4233 new_ast,
4234 sketch_id,
4235 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
4236 )?;
4237 Ok(sketch_block_ref)
4238 }
4239
4240 async fn add_horizontal(
4241 &mut self,
4242 sketch: ObjectId,
4243 horizontal: Horizontal,
4244 new_ast: &mut ast::Node<ast::Program>,
4245 ) -> Result<AstNodeRef, KclError> {
4246 let sketch_id = sketch;
4247
4248 let first_arg_ast = match horizontal {
4250 Horizontal::Line { line } => {
4251 let line_object = self
4252 .scene_graph
4253 .objects
4254 .get(line.0)
4255 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4256 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4257 let kind = line_object.kind.human_friendly_kind_with_article();
4258 return Err(KclError::refactor(format!(
4259 "This constraint only works on Segments, but you selected {kind}"
4260 )));
4261 };
4262 let Segment::Line(_) = line_segment else {
4263 return Err(KclError::refactor(format!(
4264 "Only lines can be made horizontal, but you selected {}",
4265 line_segment.human_friendly_kind_with_article(),
4266 )));
4267 };
4268 self.line_id_to_ast_reference(line, new_ast)?
4269 }
4270 Horizontal::Points { points } => {
4271 let point_asts = points
4272 .iter()
4273 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4274 .collect::<Result<Vec<_>, _>>()?;
4275 ast::ArrayExpression::new(point_asts).into()
4276 }
4277 };
4278 let horizontal_ast = create_horizontal_ast(first_arg_ast);
4280
4281 let (sketch_block_ref, _) = self.mutate_ast(
4283 new_ast,
4284 sketch_id,
4285 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4286 )?;
4287 Ok(sketch_block_ref)
4288 }
4289
4290 async fn add_lines_equal_length(
4291 &mut self,
4292 sketch: ObjectId,
4293 lines_equal_length: LinesEqualLength,
4294 new_ast: &mut ast::Node<ast::Program>,
4295 ) -> Result<AstNodeRef, KclError> {
4296 if lines_equal_length.lines.len() < 2 {
4297 return Err(KclError::refactor(format!(
4298 "Lines equal length constraint must have at least 2 lines, got {}",
4299 lines_equal_length.lines.len()
4300 )));
4301 };
4302
4303 let sketch_id = sketch;
4304
4305 let line_asts = lines_equal_length
4307 .lines
4308 .iter()
4309 .map(|line_id| {
4310 let line_object = self
4311 .scene_graph
4312 .objects
4313 .get(line_id.0)
4314 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4315 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4316 let kind = line_object.kind.human_friendly_kind_with_article();
4317 return Err(KclError::refactor(format!(
4318 "This constraint only works on Segments, but you selected {kind}"
4319 )));
4320 };
4321 let Segment::Line(_) = line_segment else {
4322 let kind = line_segment.human_friendly_kind_with_article();
4323 return Err(KclError::refactor(format!(
4324 "Only lines can be made equal length, but you selected {kind}"
4325 )));
4326 };
4327
4328 self.line_id_to_ast_reference(*line_id, new_ast)
4329 })
4330 .collect::<Result<Vec<_>, _>>()?;
4331
4332 let equal_length_ast = create_equal_length_ast(line_asts);
4334
4335 let (sketch_block_ref, _) = self.mutate_ast(
4337 new_ast,
4338 sketch_id,
4339 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4340 )?;
4341 Ok(sketch_block_ref)
4342 }
4343
4344 fn equal_radius_segment_id_to_ast_reference(
4345 &mut self,
4346 segment_id: ObjectId,
4347 new_ast: &mut ast::Node<ast::Program>,
4348 ) -> Result<ast::Expr, KclError> {
4349 let segment_object = self
4350 .scene_graph
4351 .objects
4352 .get(segment_id.0)
4353 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4354 let ObjectKind::Segment { segment } = &segment_object.kind else {
4355 return Err(KclError::refactor(format!(
4356 "Object is not a segment, it was {}",
4357 segment_object.kind.human_friendly_kind_with_article()
4358 )));
4359 };
4360
4361 let ref_type = match segment {
4362 Segment::Arc(_) => ARC_VARIABLE,
4363 Segment::Circle(_) => CIRCLE_VARIABLE,
4364 _ => {
4365 return Err(KclError::refactor(format!(
4366 "equalRadius supports only arc/circle segments, got {}",
4367 segment.human_friendly_kind_with_article()
4368 )));
4369 }
4370 };
4371
4372 get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4373 }
4374
4375 fn symmetric_input_id_to_ast_reference(
4376 &mut self,
4377 segment_id: ObjectId,
4378 new_ast: &mut ast::Node<ast::Program>,
4379 ) -> Result<ast::Expr, KclError> {
4380 let segment_object = self
4381 .scene_graph
4382 .objects
4383 .get(segment_id.0)
4384 .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4385 let ObjectKind::Segment { segment } = &segment_object.kind else {
4386 return Err(KclError::refactor(format!(
4387 "Object is not a segment, it was {}",
4388 segment_object.kind.human_friendly_kind_with_article()
4389 )));
4390 };
4391
4392 match segment {
4393 Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4394 Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4395 Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4396 Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4397 Segment::ControlPointSpline(_) => Err(KclError::refactor(
4398 "Symmetric does not yet support control point splines".to_owned(),
4399 )),
4400 }
4401 }
4402
4403 fn symmetric_axis_id_to_ast_reference(
4404 &mut self,
4405 segment_id: ObjectId,
4406 new_ast: &mut ast::Node<ast::Program>,
4407 ) -> Result<ast::Expr, KclError> {
4408 let segment_object = self
4409 .scene_graph
4410 .objects
4411 .get(segment_id.0)
4412 .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4413 let ObjectKind::Segment { segment } = &segment_object.kind else {
4414 return Err(KclError::refactor(format!(
4415 "Object is not a segment, it was {}",
4416 segment_object.kind.human_friendly_kind_with_article()
4417 )));
4418 };
4419 match segment {
4420 Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4421 _ => Err(KclError::refactor(format!(
4422 "Symmetric axis must be a line, got {}",
4423 segment.human_friendly_kind_with_article()
4424 ))),
4425 }
4426 }
4427
4428 async fn add_parallel(
4429 &mut self,
4430 sketch: ObjectId,
4431 parallel: Parallel,
4432 new_ast: &mut ast::Node<ast::Program>,
4433 ) -> Result<AstNodeRef, KclError> {
4434 if parallel.lines.len() < 2 {
4435 return Err(KclError::refactor(format!(
4436 "Parallel constraint must have at least 2 lines, got {}",
4437 parallel.lines.len()
4438 )));
4439 };
4440
4441 let sketch_id = sketch;
4442
4443 let line_asts = parallel
4444 .lines
4445 .iter()
4446 .map(|line_id| {
4447 let line_object = self
4448 .scene_graph
4449 .objects
4450 .get(line_id.0)
4451 .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4452 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4453 let kind = line_object.kind.human_friendly_kind_with_article();
4454 return Err(KclError::refactor(format!(
4455 "This constraint only works on Segments, but you selected {kind}"
4456 )));
4457 };
4458 let Segment::Line(_) = line_segment else {
4459 let kind = line_segment.human_friendly_kind_with_article();
4460 return Err(KclError::refactor(format!(
4461 "Only lines can be made parallel, but you selected {kind}"
4462 )));
4463 };
4464
4465 self.line_id_to_ast_reference(*line_id, new_ast)
4466 })
4467 .collect::<Result<Vec<_>, _>>()?;
4468
4469 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4470 callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4471 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4472 ast::ArrayExpression {
4473 elements: line_asts,
4474 digest: None,
4475 non_code_meta: Default::default(),
4476 },
4477 )))),
4478 arguments: Default::default(),
4479 digest: None,
4480 non_code_meta: Default::default(),
4481 })));
4482
4483 let (sketch_block_ref, _) = self.mutate_ast(
4484 new_ast,
4485 sketch_id,
4486 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4487 )?;
4488 Ok(sketch_block_ref)
4489 }
4490
4491 async fn add_perpendicular(
4492 &mut self,
4493 sketch: ObjectId,
4494 perpendicular: Perpendicular,
4495 new_ast: &mut ast::Node<ast::Program>,
4496 ) -> Result<AstNodeRef, KclError> {
4497 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4498 .await
4499 }
4500
4501 async fn add_lines_at_angle_constraint(
4502 &mut self,
4503 sketch: ObjectId,
4504 angle_kind: LinesAtAngleKind,
4505 lines: Vec<ObjectId>,
4506 new_ast: &mut ast::Node<ast::Program>,
4507 ) -> Result<AstNodeRef, KclError> {
4508 let &[line0_id, line1_id] = lines.as_slice() else {
4509 return Err(KclError::refactor(format!(
4510 "{} constraint must have exactly 2 lines, got {}",
4511 angle_kind.to_function_name(),
4512 lines.len()
4513 )));
4514 };
4515
4516 let sketch_id = sketch;
4517
4518 let line0_object = self
4520 .scene_graph
4521 .objects
4522 .get(line0_id.0)
4523 .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4524 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4525 let kind = line0_object.kind.human_friendly_kind_with_article();
4526 return Err(KclError::refactor(format!(
4527 "This constraint only works on Segments, but you selected {kind}"
4528 )));
4529 };
4530 let Segment::Line(_) = line0_segment else {
4531 return Err(KclError::refactor(format!(
4532 "Only lines can be made {}, but you selected {}",
4533 angle_kind.to_function_name(),
4534 line0_segment.human_friendly_kind_with_article(),
4535 )));
4536 };
4537 let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4538
4539 let line1_object = self
4540 .scene_graph
4541 .objects
4542 .get(line1_id.0)
4543 .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4544 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4545 let kind = line1_object.kind.human_friendly_kind_with_article();
4546 return Err(KclError::refactor(format!(
4547 "This constraint only works on Segments, but you selected {kind}"
4548 )));
4549 };
4550 let Segment::Line(_) = line1_segment else {
4551 return Err(KclError::refactor(format!(
4552 "Only lines can be made {}, but you selected {}",
4553 angle_kind.to_function_name(),
4554 line1_segment.human_friendly_kind_with_article(),
4555 )));
4556 };
4557 let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4558
4559 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
4561 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4562 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
4563 ast::ArrayExpression {
4564 elements: vec![line0_ast, line1_ast],
4565 digest: None,
4566 non_code_meta: Default::default(),
4567 },
4568 )))),
4569 arguments: Default::default(),
4570 digest: None,
4571 non_code_meta: Default::default(),
4572 })));
4573
4574 let (sketch_block_ref, _) = self.mutate_ast(
4576 new_ast,
4577 sketch_id,
4578 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4579 )?;
4580 Ok(sketch_block_ref)
4581 }
4582
4583 async fn add_vertical(
4584 &mut self,
4585 sketch: ObjectId,
4586 vertical: Vertical,
4587 new_ast: &mut ast::Node<ast::Program>,
4588 ) -> Result<AstNodeRef, KclError> {
4589 let sketch_id = sketch;
4590
4591 let first_arg_ast = match vertical {
4592 Vertical::Line { line } => {
4593 let line_object = self
4595 .scene_graph
4596 .objects
4597 .get(line.0)
4598 .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4599 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4600 let kind = line_object.kind.human_friendly_kind_with_article();
4601 return Err(KclError::refactor(format!(
4602 "This constraint only works on Segments, but you selected {kind}"
4603 )));
4604 };
4605 let Segment::Line(_) = line_segment else {
4606 return Err(KclError::refactor(format!(
4607 "Only lines can be made vertical, but you selected {}",
4608 line_segment.human_friendly_kind_with_article()
4609 )));
4610 };
4611 self.line_id_to_ast_reference(line, new_ast)?
4612 }
4613 Vertical::Points { points } => {
4614 let point_asts = points
4615 .iter()
4616 .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4617 .collect::<Result<Vec<_>, _>>()?;
4618 ast::ArrayExpression::new(point_asts).into()
4619 }
4620 };
4621 let vertical_ast = create_vertical_ast(first_arg_ast);
4623
4624 let (sketch_block_ref, _) = self.mutate_ast(
4626 new_ast,
4627 sketch_id,
4628 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4629 )?;
4630 Ok(sketch_block_ref)
4631 }
4632
4633 async fn execute_after_add_constraint(
4634 &mut self,
4635 ctx: &ExecutorContext,
4636 sketch_id: ObjectId,
4637 sketch_block_ref: AstNodeRef,
4638 new_ast: &mut ast::Node<ast::Program>,
4639 ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4640 let new_source = source_from_ast(new_ast);
4642 let (new_program, errors) = Program::parse(&new_source)
4644 .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
4645 if !errors.is_empty() {
4646 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4647 "Error parsing KCL source after adding constraint: {errors:?}"
4648 ))));
4649 }
4650 let Some(new_program) = new_program else {
4651 return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
4652 "No AST produced after adding constraint".to_string(),
4653 )));
4654 };
4655 let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4656 KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4657 "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4658 )))
4659 })?;
4660
4661 let mut truncated_program = new_program.clone();
4664 only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4665 .map_err(KclErrorWithOutputs::no_outputs)?;
4666
4667 let outcome = ctx
4669 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4670 .await?;
4671
4672 let new_object_ids = {
4673 let constraint_id = outcome
4675 .source_range_to_object
4676 .get(&constraint_node_ref.range)
4677 .copied()
4678 .ok_or_else(|| {
4679 KclErrorWithOutputs::from_error_outcome(
4680 KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4681 outcome.clone(),
4682 )
4683 })?;
4684 vec![constraint_id]
4685 };
4686
4687 self.program = new_program;
4690
4691 let outcome = self.update_state_after_exec(outcome, true);
4693
4694 let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4695 let scene_graph_delta = SceneGraphDelta {
4696 new_graph: self.scene_graph_for_ui(),
4697 invalidates_ids: false,
4698 new_objects: new_object_ids,
4699 exec_outcome: outcome,
4700 };
4701 Ok((src_delta, scene_graph_delta))
4702 }
4703
4704 fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4705 let commit_failure = || {
4706 KclErrorWithOutputs::from_error_outcome(
4707 KclError::refactor(format!("Could not update KCL after {operation}.")),
4708 outcome.clone(),
4709 )
4710 };
4711
4712 let default_length_unit = self.default_length_unit();
4713 let mut settled_ast = self.program.ast.clone();
4714 let mut committed_solver_value = false;
4715 for (var_range, node_path, value) in &outcome.var_solutions {
4716 let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4717 return Err(commit_failure());
4718 };
4719 let new_value = match &lookup {
4720 Some(current_literal) => {
4721 if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4722 continue;
4723 }
4724 preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4725 }
4726 None => {
4727 Number {
4731 value: number_value_in_default_length_units(*value, default_length_unit),
4732 units: default_length_unit.into(),
4733 }
4734 }
4735 };
4736 committed_solver_value = true;
4737 let source_ref = SourceRef::Simple {
4738 range: *var_range,
4739 node_path: node_path.clone(),
4740 };
4741 mutate_ast_node_by_source_ref(
4742 &mut settled_ast,
4743 &source_ref,
4744 AstMutateCommand::EditVarInitialValue { value: new_value },
4745 )
4746 .map_err(|_| commit_failure())?;
4747 }
4748
4749 if !committed_solver_value {
4750 return Ok(SourceDelta {
4751 text: self.program.original_file_contents.clone(),
4752 });
4753 }
4754
4755 let settled_source = source_from_ast(&settled_ast);
4756 let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4757 if !errors.is_empty() {
4758 return Err(commit_failure());
4759 }
4760 let Some(settled_program) = settled_program else {
4761 return Err(commit_failure());
4762 };
4763
4764 self.program = settled_program;
4765
4766 Ok(SourceDelta { text: settled_source })
4767 }
4768
4769 fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4771 if segment_ids_set.contains(&segment_id) {
4772 return true;
4773 }
4774
4775 let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4776 return false;
4777 };
4778 let ObjectKind::Segment { segment } = &segment_object.kind else {
4779 return false;
4780 };
4781 let Segment::Point(point) = segment else {
4782 return false;
4783 };
4784
4785 point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4786 }
4787
4788 fn remaining_constraint_segments(
4789 &self,
4790 segments: &[ConstraintSegment],
4791 segment_ids_set: &AhashIndexSet<ObjectId>,
4792 ) -> Vec<ConstraintSegment> {
4793 segments
4794 .iter()
4795 .copied()
4796 .filter(|segment| match segment {
4797 ConstraintSegment::Origin(_) => true,
4798 ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4799 })
4800 .collect()
4801 }
4802
4803 fn find_referenced_constraints(
4804 &self,
4805 sketch_id: ObjectId,
4806 segment_ids_set: &AhashIndexSet<ObjectId>,
4807 ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4808 let sketch_object = self
4810 .scene_graph
4811 .objects
4812 .get(sketch_id.0)
4813 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4814 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4815 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4816 };
4817 let segment_or_owner_matches = |segment_id: ObjectId| {
4818 if segment_ids_set.contains(&segment_id) {
4819 return true;
4820 }
4821 let segment_object = self.scene_graph.objects.get(segment_id.0);
4822 if let Some(obj) = segment_object
4823 && let ObjectKind::Segment { segment } = &obj.kind
4824 {
4825 match segment {
4826 Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4827 Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4828 _ => false,
4829 }
4830 } else {
4831 false
4832 }
4833 };
4834 let mut constraint_ids_set = AhashIndexSet::default();
4835 for constraint_id in &sketch.constraints {
4836 let constraint_object = self
4837 .scene_graph
4838 .objects
4839 .get(constraint_id.0)
4840 .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4841 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4842 return Err(KclError::refactor(format!(
4843 "Object is not a constraint, it is {}",
4844 constraint_object.kind.human_friendly_kind_with_article()
4845 )));
4846 };
4847 let depends_on_segment = match constraint {
4848 Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4849 Constraint::Distance(d) => d.point_ids().any(segment_or_owner_matches),
4850 Constraint::Fixed(fixed) => fixed
4851 .points
4852 .iter()
4853 .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4854 Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4855 Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4856 Constraint::EqualRadius(equal_radius) => {
4857 equal_radius.input.iter().copied().any(segment_or_owner_matches)
4858 }
4859 Constraint::HorizontalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4860 Constraint::VerticalDistance(d) => d.point_ids().any(segment_or_owner_matches),
4861 Constraint::Horizontal(h) => match h {
4862 Horizontal::Line { line } => segment_or_owner_matches(*line),
4863 Horizontal::Points { points } => points.iter().any(|point| match point {
4864 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4865 ConstraintSegment::Origin(_) => false,
4866 }),
4867 },
4868 Constraint::Vertical(v) => match v {
4869 Vertical::Line { line } => segment_or_owner_matches(*line),
4870 Vertical::Points { points } => points.iter().any(|point| match point {
4871 ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4872 ConstraintSegment::Origin(_) => false,
4873 }),
4874 },
4875 Constraint::LinesEqualLength(lines_equal_length) => {
4876 lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4877 }
4878 Constraint::Midpoint(midpoint) => {
4879 segment_or_owner_matches(midpoint.segment)
4880 || matches!(
4881 midpoint.point,
4882 ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4883 )
4884 }
4885 Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4886 Constraint::Perpendicular(perpendicular) => {
4887 perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4888 }
4889 Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4890 Constraint::Symmetric(symmetric) => {
4891 segment_or_owner_matches(symmetric.axis)
4892 || symmetric.input.iter().copied().any(segment_or_owner_matches)
4893 }
4894 Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4895 };
4896 if depends_on_segment {
4897 constraint_ids_set.insert(*constraint_id);
4898 }
4899 }
4900 Ok(constraint_ids_set)
4901 }
4902
4903 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4904 let mut outcome = outcome;
4905 let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4906
4907 if freedom_analysis_ran {
4908 self.point_freedom_cache.clear();
4911 for new_obj in &new_objects {
4912 if let ObjectKind::Segment {
4913 segment: crate::front::Segment::Point(point),
4914 } = &new_obj.kind
4915 {
4916 self.point_freedom_cache.insert(new_obj.id, point.freedom);
4917 }
4918 }
4919 add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4920 self.scene_graph.objects = new_objects;
4922 } else {
4923 for old_obj in &self.scene_graph.objects {
4926 if let ObjectKind::Segment {
4927 segment: crate::front::Segment::Point(point),
4928 } = &old_obj.kind
4929 {
4930 self.point_freedom_cache.insert(old_obj.id, point.freedom);
4931 }
4932 }
4933
4934 let mut updated_objects = Vec::with_capacity(new_objects.len());
4936 for new_obj in new_objects {
4937 let mut obj = new_obj;
4938 if let ObjectKind::Segment {
4939 segment: crate::front::Segment::Point(point),
4940 } = &mut obj.kind
4941 {
4942 let new_freedom = point.freedom;
4943 match new_freedom {
4949 Freedom::Free => {
4950 match self.point_freedom_cache.get(&obj.id).copied() {
4951 Some(Freedom::Conflict) => {
4952 }
4955 Some(Freedom::Fixed) => {
4956 point.freedom = Freedom::Fixed;
4958 }
4959 Some(Freedom::Free) => {
4960 }
4962 None => {
4963 }
4965 }
4966 }
4967 Freedom::Fixed => {
4968 }
4970 Freedom::Conflict => {
4971 }
4973 }
4974 self.point_freedom_cache.insert(obj.id, point.freedom);
4976 }
4977 updated_objects.push(obj);
4978 }
4979
4980 add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
4981 self.scene_graph.objects = updated_objects;
4982 }
4983 outcome
4984 }
4985
4986 fn mutate_ast(
4987 &mut self,
4988 ast: &mut ast::Node<ast::Program>,
4989 object_id: ObjectId,
4990 command: AstMutateCommand,
4991 ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
4992 let sketch_object = self
4993 .scene_graph
4994 .objects
4995 .get(object_id.0)
4996 .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
4997 mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
4998 }
4999}
5000
5001fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5002 let sketch_object = scene_graph
5004 .objects
5005 .get(sketch_id.0)
5006 .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5007 let ObjectKind::Sketch(_) = &sketch_object.kind else {
5008 return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5009 };
5010 expect_single_node_ref(sketch_object)
5011}
5012
5013fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5014 match &object.source {
5015 SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5016 range: *range,
5017 node_path: node_path.clone(),
5018 }),
5019 SourceRef::BackTrace { ranges } => {
5020 let [range] = ranges.as_slice() else {
5021 return Err(KclError::refactor(format!(
5022 "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5023 ranges.len()
5024 )));
5025 };
5026 Ok(AstNodeRef {
5027 range: range.0,
5028 node_path: range.1.clone(),
5029 })
5030 }
5031 }
5032}
5033
5034fn only_sketch_block_from_range(
5037 ast: &mut ast::Node<ast::Program>,
5038 sketch_block_range: SourceRange,
5039 edit_kind: ChangeKind,
5040) -> Result<(), KclError> {
5041 let r1 = sketch_block_range;
5042 let matches_range = |r2: SourceRange| -> bool {
5043 match edit_kind {
5046 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5047 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5049 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5050 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5052 }
5053 };
5054 let mut found = false;
5055 for item in ast.body.iter_mut() {
5056 match item {
5057 ast::BodyItem::ImportStatement(_) => {}
5058 ast::BodyItem::ExpressionStatement(node) => {
5059 if matches_range(SourceRange::from(&*node))
5060 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5061 {
5062 sketch_block.is_being_edited = true;
5063 found = true;
5064 break;
5065 }
5066 }
5067 ast::BodyItem::VariableDeclaration(node) => {
5068 if matches_range(SourceRange::from(&node.declaration.init))
5069 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5070 {
5071 sketch_block.is_being_edited = true;
5072 found = true;
5073 break;
5074 }
5075 }
5076 ast::BodyItem::TypeDeclaration(_) => {}
5077 ast::BodyItem::ReturnStatement(node) => {
5078 if matches_range(SourceRange::from(&node.argument))
5079 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5080 {
5081 sketch_block.is_being_edited = true;
5082 found = true;
5083 break;
5084 }
5085 }
5086 }
5087 }
5088 if !found {
5089 return Err(KclError::refactor(format!(
5090 "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5091 )));
5092 }
5093
5094 Ok(())
5095}
5096
5097fn only_sketch_block(
5098 ast: &mut ast::Node<ast::Program>,
5099 sketch_block_ref: &AstNodeRef,
5100 edit_kind: ChangeKind,
5101) -> Result<(), KclError> {
5102 let Some(target_node_path) = &sketch_block_ref.node_path else {
5103 #[cfg(target_arch = "wasm32")]
5104 web_sys::console::warn_1(
5105 &format!(
5106 "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5107 &sketch_block_ref
5108 )
5109 .into(),
5110 );
5111 return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5112 };
5113 let mut found = false;
5114 for item in ast.body.iter_mut() {
5115 match item {
5116 ast::BodyItem::ImportStatement(_) => {}
5117 ast::BodyItem::ExpressionStatement(node) => {
5118 if let Some(node_path) = &node.node_path
5120 && node_path == target_node_path
5121 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5122 {
5123 sketch_block.is_being_edited = true;
5124 found = true;
5125 break;
5126 }
5127 if let Some(node_path) = node.expression.node_path()
5129 && node_path == target_node_path
5130 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5131 {
5132 sketch_block.is_being_edited = true;
5133 found = true;
5134 break;
5135 }
5136 }
5137 ast::BodyItem::VariableDeclaration(node) => {
5138 if let Some(node_path) = node.declaration.init.node_path()
5139 && node_path == target_node_path
5140 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5141 {
5142 sketch_block.is_being_edited = true;
5143 found = true;
5144 break;
5145 }
5146 }
5147 ast::BodyItem::TypeDeclaration(_) => {}
5148 ast::BodyItem::ReturnStatement(node) => {
5149 if let Some(node_path) = node.argument.node_path()
5150 && node_path == target_node_path
5151 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5152 {
5153 sketch_block.is_being_edited = true;
5154 found = true;
5155 break;
5156 }
5157 }
5158 }
5159 }
5160 if !found {
5161 return Err(KclError::refactor(format!(
5162 "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5163 )));
5164 }
5165
5166 Ok(())
5167}
5168
5169fn sketch_on_ast_expr(
5170 ast: &mut ast::Node<ast::Program>,
5171 scene_graph: &SceneGraph,
5172 on: &Plane,
5173) -> Result<ast::Expr, KclError> {
5174 match on {
5175 Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5176 Plane::Object(object_id) => {
5177 let on_object = scene_graph
5178 .objects
5179 .get(object_id.0)
5180 .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5181 if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5182 return Ok(face_expr);
5183 }
5184 get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5185 }
5186 }
5187}
5188
5189fn sketch_face_of_scene_object_ast_expr(
5190 ast: &mut ast::Node<ast::Program>,
5191 on_object: &crate::front::Object,
5192) -> Result<Option<ast::Expr>, KclError> {
5193 let SourceRef::BackTrace { ranges } = &on_object.source else {
5194 return Ok(None);
5195 };
5196
5197 match &on_object.kind {
5198 ObjectKind::Wall(_) => {
5199 let (solid_range, sweep_range, segment_range) = match ranges.as_slice() {
5200 [sweep_range, segment_range] => (sweep_range, sweep_range, segment_range),
5201 [solid_range, sweep_range, segment_range] => (solid_range, sweep_range, segment_range),
5202 _ => {
5203 return Err(KclError::refactor(format!(
5204 "Expected wall source metadata to have 2 or 3 ranges, got {}; artifact_id={:?}",
5205 ranges.len(),
5206 on_object.artifact_id
5207 )));
5208 }
5209 };
5210 let solid_ref = get_or_insert_ast_reference(
5211 ast,
5212 &SourceRef::Simple {
5213 range: solid_range.0,
5214 node_path: solid_range.1.clone(),
5215 },
5216 "solid",
5217 None,
5218 )?;
5219 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5220 return Err(KclError::refactor(format!(
5221 "Could not resolve solid reference for selected wall: artifact_id={:?}",
5222 on_object.artifact_id
5223 )));
5224 };
5225 let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
5226 let sweep_ref = get_or_insert_ast_reference(
5227 ast,
5228 &SourceRef::Simple {
5229 range: sweep_range.0,
5230 node_path: sweep_range.1.clone(),
5231 },
5232 "solid",
5233 None,
5234 )?;
5235 let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5236 return Err(KclError::refactor(format!(
5237 "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5238 on_object.artifact_id
5239 )));
5240 };
5241 let sweep_name = sweep_name_expr.name.name.clone();
5242 let segment_ref = get_or_insert_ast_reference(
5243 ast,
5244 &SourceRef::Simple {
5245 range: segment_range.0,
5246 node_path: segment_range.1.clone(),
5247 },
5248 LINE_VARIABLE,
5249 None,
5250 )?;
5251
5252 let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name) {
5253 let ast::Expr::Name(segment_name_expr) = segment_ref else {
5254 return Err(KclError::refactor(format!(
5255 "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5256 on_object.artifact_id
5257 )));
5258 };
5259 create_member_expression(
5260 create_member_expression(ast_name_expr(region_name), "tags"),
5261 &segment_name_expr.name.name,
5262 )
5263 } else {
5264 segment_ref
5265 };
5266
5267 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5268 }
5269 ObjectKind::Cap(cap) => {
5270 let solid_range = match ranges.as_slice() {
5271 [solid_range] | [solid_range, _] => solid_range,
5272 _ => {
5273 return Err(KclError::refactor(format!(
5274 "Expected cap source metadata to have 1 or 2 ranges, got {}; artifact_id={:?}",
5275 ranges.len(),
5276 on_object.artifact_id
5277 )));
5278 }
5279 };
5280 let solid_ref = get_or_insert_ast_reference(
5281 ast,
5282 &SourceRef::Simple {
5283 range: solid_range.0,
5284 node_path: solid_range.1.clone(),
5285 },
5286 "solid",
5287 None,
5288 )?;
5289 let ast::Expr::Name(solid_name_expr) = solid_ref else {
5290 return Err(KclError::refactor(format!(
5291 "Could not resolve solid reference for selected cap: artifact_id={:?}",
5292 on_object.artifact_id
5293 )));
5294 };
5295 let solid_expr = ast_name_expr(solid_name_expr.name.name.clone());
5296 let face_expr = match cap.kind {
5298 crate::frontend::api::CapKind::Start => ast_name_expr("START".to_owned()),
5299 crate::frontend::api::CapKind::End => ast_name_expr("END".to_owned()),
5300 };
5301
5302 Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5303 }
5304 _ => Ok(None),
5305 }
5306}
5307
5308fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5309 let mut current_id = source_id;
5310 let mut current_composite = None;
5311 let mut visited = HashSet::new();
5312
5313 while visited.insert(current_id) {
5314 let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5315
5316 let Some(composite_id) = next_composite_id else {
5317 break;
5318 };
5319 let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5320 break;
5321 };
5322
5323 current_id = composite.id;
5324 current_composite = Some(composite);
5325
5326 if !composite.consumed {
5327 break;
5328 }
5329 }
5330
5331 current_composite.map(|composite| &composite.code_ref)
5332}
5333
5334fn downstream_composite_id_for_solid_source(
5335 artifact_graph: &ArtifactGraph,
5336 source_id: ArtifactId,
5337) -> Option<ArtifactId> {
5338 if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5340 && let Some(composite_id) = path.composite_solid_id
5341 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5342 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5343 {
5344 return Some(composite_id);
5345 }
5346
5347 for artifact in artifact_graph.values() {
5349 if let Artifact::Path(path) = artifact
5350 && path.sweep_id == Some(source_id)
5351 && let Some(composite_id) = path.composite_solid_id
5352 && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5353 && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5354 {
5355 return Some(composite_id);
5356 }
5357 }
5358
5359 artifact_graph.values().find_map(|artifact| {
5361 let Artifact::CompositeSolid(composite) = artifact else {
5362 return None;
5363 };
5364 composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5365 })
5366}
5367
5368fn composite_contains_path_input(
5369 solid_ids: &[ArtifactId],
5370 tool_ids: &[ArtifactId],
5371 path_id: ArtifactId,
5372 solid2d_id: Option<ArtifactId>,
5373) -> bool {
5374 composite_contains_input(solid_ids, tool_ids, path_id)
5375 || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5376}
5377
5378fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5379 solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5380}
5381
5382fn code_ref_source_ref_range(code_ref: &CodeRef) -> (SourceRange, Option<crate::NodePath>) {
5383 let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5384 (code_ref.range, node_path)
5385}
5386
5387fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<crate::front::Object>, artifact_graph: &ArtifactGraph) {
5388 let mut existing_artifact_ids = scene_objects
5389 .iter()
5390 .map(|object| object.artifact_id)
5391 .collect::<HashSet<_>>();
5392
5393 for artifact in artifact_graph.values() {
5394 match artifact {
5395 Artifact::Wall(wall) => {
5396 if existing_artifact_ids.contains(&wall.id) {
5397 continue;
5398 }
5399
5400 let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5401 Artifact::Segment(segment) => Some(segment),
5402 _ => None,
5403 }) else {
5404 continue;
5405 };
5406 let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5407 Artifact::Sweep(sweep) => Some(sweep),
5408 _ => None,
5409 }) else {
5410 continue;
5411 };
5412 let source_segment = segment
5413 .original_seg_id
5414 .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5415 .and_then(|artifact| match artifact {
5416 Artifact::Segment(segment) => Some(segment),
5417 _ => None,
5418 })
5419 .unwrap_or(segment);
5420 let solid_code_ref =
5421 downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5422 let mut ranges = Vec::new();
5423 if solid_code_ref.range != sweep.code_ref.range || solid_code_ref.node_path != sweep.code_ref.node_path
5424 {
5425 ranges.push(code_ref_source_ref_range(solid_code_ref));
5426 }
5427 ranges.push(code_ref_source_ref_range(&sweep.code_ref));
5428 ranges.push(code_ref_source_ref_range(&source_segment.code_ref));
5429 let id = ObjectId(scene_objects.len());
5430 scene_objects.push(crate::front::Object {
5431 id,
5432 kind: ObjectKind::Wall(crate::frontend::api::Wall { id }),
5433 label: Default::default(),
5434 comments: Default::default(),
5435 artifact_id: wall.id,
5436 source: SourceRef::BackTrace { ranges },
5437 });
5438 existing_artifact_ids.insert(wall.id);
5439 }
5440 Artifact::Cap(cap) => {
5441 if existing_artifact_ids.contains(&cap.id) {
5442 continue;
5443 }
5444
5445 let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5446 Artifact::Sweep(sweep) => Some(sweep),
5447 _ => None,
5448 }) else {
5449 continue;
5450 };
5451 let id = ObjectId(scene_objects.len());
5452 let kind = match cap.sub_type {
5453 CapSubType::Start => crate::frontend::api::CapKind::Start,
5454 CapSubType::End => crate::frontend::api::CapKind::End,
5455 };
5456 let solid_code_ref =
5457 downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5458 let mut ranges = Vec::new();
5459 if solid_code_ref.range != sweep.code_ref.range || solid_code_ref.node_path != sweep.code_ref.node_path
5460 {
5461 ranges.push(code_ref_source_ref_range(solid_code_ref));
5462 }
5463 ranges.push(code_ref_source_ref_range(&sweep.code_ref));
5464 scene_objects.push(crate::front::Object {
5465 id,
5466 kind: ObjectKind::Cap(crate::frontend::api::Cap { id, kind }),
5467 label: Default::default(),
5468 comments: Default::default(),
5469 artifact_id: cap.id,
5470 source: SourceRef::BackTrace { ranges },
5471 });
5472 existing_artifact_ids.insert(cap.id);
5473 }
5474 _ => {}
5475 }
5476 }
5477}
5478
5479fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5480 use crate::engine::PlaneName;
5481
5482 match name {
5483 PlaneName::Xy => ast_name_expr("XY".to_owned()),
5484 PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5485 PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5486 PlaneName::NegXy => negated_plane_ast_expr("XY"),
5487 PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5488 PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5489 }
5490}
5491
5492fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5493 ast::Expr::UnaryExpression(Box::new(ast::UnaryExpression::new(
5494 ast::UnaryOperator::Neg,
5495 ast::BinaryPart::Name(Box::new(ast_name(name.to_owned()))),
5496 )))
5497}
5498
5499fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5500 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
5501 callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5502 unlabeled: Some(solid_expr),
5503 arguments: vec![ast::LabeledArg {
5504 label: Some(ast::Identifier::new("face")),
5505 arg: face_expr,
5506 }],
5507 digest: None,
5508 non_code_meta: Default::default(),
5509 })))
5510}
5511
5512fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5513 let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5514 return None;
5515 };
5516 let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5517 return None;
5518 };
5519 if !matches!(
5520 sweep_call.callee.name.name.as_str(),
5521 "extrude" | "revolve" | "sweep" | "loft"
5522 ) {
5523 return None;
5524 }
5525 let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5526 return None;
5527 };
5528 let candidate = region_name_expr.name.name.clone();
5529 let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5530 return None;
5531 };
5532 let ast::Expr::CallExpressionKw(region_call) = ®ion_decl.init else {
5533 return None;
5534 };
5535 if region_call.callee.name.name != "region" {
5536 return None;
5537 }
5538 Some(candidate)
5539}
5540
5541fn get_or_insert_ast_reference(
5548 ast: &mut ast::Node<ast::Program>,
5549 source_ref: &SourceRef,
5550 prefix: &str,
5551 property: Option<&str>,
5552) -> Result<ast::Expr, KclError> {
5553 let command = AstMutateCommand::AddVariableDeclaration {
5554 prefix: prefix.to_owned(),
5555 };
5556 let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5557 Ok((_, ret)) => ret,
5558 Err(err) => {
5559 if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5560 AstMutateCommandReturn::Name(var_name)
5561 } else {
5562 return Err(err);
5563 }
5564 }
5565 };
5566 let AstMutateCommandReturn::Name(var_name) = ret else {
5567 return Err(KclError::refactor(
5568 "Expected variable name returned from AddVariableDeclaration".to_owned(),
5569 ));
5570 };
5571 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
5572 let Some(property) = property else {
5573 return Ok(var_expr);
5575 };
5576
5577 Ok(create_member_expression(var_expr, property))
5578}
5579
5580fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5581 let source_range = match source_ref {
5582 SourceRef::Simple { range, .. } => *range,
5583 SourceRef::BackTrace { ranges } => {
5584 let [range] = ranges.as_slice() else {
5585 return None;
5586 };
5587 range.0
5588 }
5589 };
5590 ast.body.iter().find_map(|item| {
5591 let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5592 return None;
5593 };
5594 let init_range = SourceRange::from(&var_decl.declaration.init);
5595 let source_is_inside_init = init_range.module_id() == source_range.module_id()
5596 && init_range.start() <= source_range.start()
5597 && source_range.end() <= init_range.end();
5598 if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5599 && init_range != source_range
5600 && source_is_inside_init
5601 {
5602 return None;
5603 }
5604 source_is_inside_init.then(|| var_decl.name().to_owned())
5605 })
5606}
5607
5608fn mutate_ast_node_by_source_ref(
5609 ast: &mut ast::Node<ast::Program>,
5610 source_ref: &SourceRef,
5611 command: AstMutateCommand,
5612) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5613 let (source_range, node_path) = match source_ref {
5614 SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5615 SourceRef::BackTrace { ranges } => {
5616 let [range] = ranges.as_slice() else {
5617 return Err(KclError::refactor(format!(
5618 "Expected single source ref, got {}; ranges={ranges:#?}",
5619 ranges.len(),
5620 )));
5621 };
5622 (range.0, range.1.clone())
5623 }
5624 };
5625 let mut context = AstMutateContext {
5626 source_range,
5627 node_path,
5628 command,
5629 defined_names_stack: Default::default(),
5630 };
5631 let control = dfs_mut(ast, &mut context);
5632 match control {
5633 ControlFlow::Continue(_) => Err(KclError::refactor(
5634 "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5635 )),
5636 ControlFlow::Break(break_value) => break_value,
5637 }
5638}
5639
5640#[derive(Debug)]
5641struct AstMutateContext {
5642 source_range: SourceRange,
5643 node_path: Option<ast::NodePath>,
5644 command: AstMutateCommand,
5645 defined_names_stack: Vec<HashSet<String>>,
5646}
5647
5648#[derive(Debug)]
5649#[allow(clippy::large_enum_variant)]
5650enum AstMutateCommand {
5651 AddSketchBlockExprStmt {
5653 expr: ast::Expr,
5654 },
5655 AddSketchBlockVarDecl {
5657 prefix: String,
5658 expr: ast::Expr,
5659 },
5660 AddVariableDeclaration {
5661 prefix: String,
5662 },
5663 EditPoint {
5664 at: ast::Expr,
5665 },
5666 EditLine {
5667 start: ast::Expr,
5668 end: ast::Expr,
5669 construction: Option<bool>,
5670 },
5671 EditArc {
5672 start: ast::Expr,
5673 end: ast::Expr,
5674 center: ast::Expr,
5675 construction: Option<bool>,
5676 },
5677 EditCircle {
5678 start: ast::Expr,
5679 center: ast::Expr,
5680 construction: Option<bool>,
5681 },
5682 EditControlPointSpline {
5683 points: ast::Expr,
5684 construction: Option<bool>,
5685 },
5686 EditConstraintValue {
5687 value: ast::BinaryPart,
5688 },
5689 EditDistanceConstraintLabelPosition {
5690 label_position: ast::Expr,
5691 },
5692 EditCallUnlabeled {
5693 arg: ast::Expr,
5694 },
5695 EditVarInitialValue {
5696 value: Number,
5697 },
5698 DeleteNode,
5699}
5700
5701impl AstMutateCommand {
5702 fn needs_defined_names_stack(&self) -> bool {
5703 matches!(
5704 self,
5705 AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5706 )
5707 }
5708}
5709
5710#[derive(Debug)]
5711enum AstMutateCommandReturn {
5712 None,
5713 Name(String),
5714}
5715
5716#[derive(Debug, Clone)]
5717struct AstNodeRef {
5718 range: SourceRange,
5719 node_path: Option<ast::NodePath>,
5720}
5721
5722impl<T> From<&ast::Node<T>> for AstNodeRef {
5723 fn from(value: &ast::Node<T>) -> Self {
5724 AstNodeRef {
5725 range: value.into(),
5726 node_path: value.node_path.clone(),
5727 }
5728 }
5729}
5730
5731impl From<&ast::BodyItem> for AstNodeRef {
5732 fn from(value: &ast::BodyItem) -> Self {
5733 match value {
5734 ast::BodyItem::ImportStatement(node) => AstNodeRef {
5735 range: node.into(),
5736 node_path: node.node_path.clone(),
5737 },
5738 ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5739 range: node.into(),
5740 node_path: node.node_path.clone(),
5741 },
5742 ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5743 range: node.into(),
5744 node_path: node.node_path.clone(),
5745 },
5746 ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5747 range: node.into(),
5748 node_path: node.node_path.clone(),
5749 },
5750 ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5751 range: node.into(),
5752 node_path: node.node_path.clone(),
5753 },
5754 }
5755 }
5756}
5757
5758impl From<&ast::Expr> for AstNodeRef {
5759 fn from(value: &ast::Expr) -> Self {
5760 AstNodeRef {
5761 range: SourceRange::from(value),
5762 node_path: value.node_path().cloned(),
5763 }
5764 }
5765}
5766
5767impl From<&AstMutateContext> for AstNodeRef {
5768 fn from(value: &AstMutateContext) -> Self {
5769 AstNodeRef {
5770 range: value.source_range,
5771 node_path: value.node_path.clone(),
5772 }
5773 }
5774}
5775
5776impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5777 type Error = crate::walk::AstNodeError;
5778
5779 fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5780 Ok(AstNodeRef {
5781 range: SourceRange::try_from(value)?,
5782 node_path: value.try_into()?,
5783 })
5784 }
5785}
5786
5787impl From<AstNodeRef> for SourceRange {
5788 fn from(value: AstNodeRef) -> Self {
5789 value.range
5790 }
5791}
5792
5793impl Visitor for AstMutateContext {
5794 type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5795 type Continue = ();
5796
5797 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5798 filter_and_process(self, node)
5799 }
5800
5801 fn finish(&mut self, node: NodeMut<'_>) {
5802 match &node {
5803 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5804 self.defined_names_stack.pop();
5805 }
5806 _ => {}
5807 }
5808 }
5809}
5810
5811fn filter_and_process(
5812 ctx: &mut AstMutateContext,
5813 node: NodeMut,
5814) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5815 let Ok(node_range) = SourceRange::try_from(&node) else {
5816 return TraversalReturn::new_continue(());
5818 };
5819 if let NodeMut::VariableDeclaration(var_decl) = &node {
5824 let expr_range = SourceRange::from(&var_decl.declaration.init);
5825 let expr_node_path = var_decl.declaration.init.node_path();
5826 if source_ref_matches(ctx, expr_range, expr_node_path) {
5827 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5828 return TraversalReturn::new_break(Ok((
5831 AstNodeRef::from(&**var_decl),
5832 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
5833 )));
5834 }
5835 if let AstMutateCommand::DeleteNode = &ctx.command {
5836 return TraversalReturn {
5839 mutate_body_item: MutateBodyItem::Delete,
5840 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5841 };
5842 }
5843 }
5844 }
5845 if let NodeMut::ExpressionStatement(expr_stmt) = &node {
5848 let expr_range = SourceRange::from(&expr_stmt.expression);
5849 let expr_node_path = expr_stmt.expression.node_path();
5850 if source_ref_matches(ctx, expr_range, expr_node_path) {
5851 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
5852 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5855 return TraversalReturn::new_continue(());
5856 };
5857 return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
5858 }
5859 if let AstMutateCommand::DeleteNode = &ctx.command {
5860 return TraversalReturn {
5863 mutate_body_item: MutateBodyItem::Delete,
5864 control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
5865 };
5866 }
5867 }
5868 }
5869
5870 if ctx.command.needs_defined_names_stack() {
5871 if let NodeMut::Program(program) = &node {
5872 ctx.defined_names_stack.push(find_defined_names(*program));
5873 } else if let NodeMut::SketchBlock(block) = &node {
5874 ctx.defined_names_stack.push(find_defined_names(&block.body));
5875 }
5876 }
5877
5878 let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
5880 if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
5881 return TraversalReturn::new_continue(());
5882 }
5883 let Ok(node_ref) = AstNodeRef::try_from(&node) else {
5884 return TraversalReturn::new_continue(());
5885 };
5886 process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
5887}
5888
5889fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
5890 match &ctx.node_path {
5891 Some(target) => Some(target) == node_path,
5892 None => node_range == ctx.source_range,
5893 }
5894}
5895
5896fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
5897 match &ctx.command {
5898 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
5899 if let NodeMut::SketchBlock(sketch_block) = node {
5900 sketch_block
5901 .body
5902 .items
5903 .push(ast::BodyItem::ExpressionStatement(ast::Node {
5904 inner: ast::ExpressionStatement {
5905 expression: expr.clone(),
5906 digest: None,
5907 },
5908 start: Default::default(),
5909 end: Default::default(),
5910 module_id: Default::default(),
5911 node_path: None,
5912 outer_attrs: Default::default(),
5913 pre_comments: Default::default(),
5914 comment_start: Default::default(),
5915 }));
5916 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5917 }
5918 }
5919 AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
5920 if let NodeMut::SketchBlock(sketch_block) = node {
5921 let empty_defined_names = HashSet::new();
5922 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5923 let Ok(name) = next_free_name(prefix, defined_names) else {
5924 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5925 };
5926 sketch_block
5927 .body
5928 .items
5929 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
5930 ast::VariableDeclaration::new(
5931 ast::VariableDeclarator::new(&name, expr.clone()),
5932 ast::ItemVisibility::Default,
5933 ast::VariableKind::Const,
5934 ),
5935 ))));
5936 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
5937 }
5938 }
5939 AstMutateCommand::AddVariableDeclaration { prefix } => {
5940 if let NodeMut::VariableDeclaration(inner) = node {
5941 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
5942 }
5943 if let NodeMut::ExpressionStatement(expr_stmt) = node {
5944 let empty_defined_names = HashSet::new();
5945 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
5946 let Ok(name) = next_free_name(prefix, defined_names) else {
5947 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5949 };
5950 let mutate_node =
5951 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
5952 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
5953 ast::ItemVisibility::Default,
5954 ast::VariableKind::Const,
5955 ))));
5956 return TraversalReturn {
5957 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
5958 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
5959 };
5960 }
5961 }
5962 AstMutateCommand::EditPoint { at } => {
5963 if let NodeMut::CallExpressionKw(call) = node {
5964 if call.callee.name.name != POINT_FN {
5965 return TraversalReturn::new_continue(());
5966 }
5967 for labeled_arg in &mut call.arguments {
5969 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
5970 labeled_arg.arg = at.clone();
5971 }
5972 }
5973 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
5974 }
5975 }
5976 AstMutateCommand::EditLine {
5977 start,
5978 end,
5979 construction,
5980 } => {
5981 if let NodeMut::CallExpressionKw(call) = node {
5982 if call.callee.name.name != LINE_FN {
5983 return TraversalReturn::new_continue(());
5984 }
5985 for labeled_arg in &mut call.arguments {
5987 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
5988 labeled_arg.arg = start.clone();
5989 }
5990 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
5991 labeled_arg.arg = end.clone();
5992 }
5993 }
5994 if let Some(construction_value) = construction {
5996 let construction_exists = call
5997 .arguments
5998 .iter()
5999 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6000 if *construction_value {
6001 if construction_exists {
6003 for labeled_arg in &mut call.arguments {
6005 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6006 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6007 value: ast::LiteralValue::Bool(true),
6008 raw: "true".to_string(),
6009 digest: None,
6010 })));
6011 }
6012 }
6013 } else {
6014 call.arguments.push(ast::LabeledArg {
6016 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6017 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6018 value: ast::LiteralValue::Bool(true),
6019 raw: "true".to_string(),
6020 digest: None,
6021 }))),
6022 });
6023 }
6024 } else {
6025 call.arguments
6027 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6028 }
6029 }
6030 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6031 }
6032 }
6033 AstMutateCommand::EditArc {
6034 start,
6035 end,
6036 center,
6037 construction,
6038 } => {
6039 if let NodeMut::CallExpressionKw(call) = node {
6040 if call.callee.name.name != ARC_FN {
6041 return TraversalReturn::new_continue(());
6042 }
6043 for labeled_arg in &mut call.arguments {
6045 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6046 labeled_arg.arg = start.clone();
6047 }
6048 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6049 labeled_arg.arg = end.clone();
6050 }
6051 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6052 labeled_arg.arg = center.clone();
6053 }
6054 }
6055 if let Some(construction_value) = construction {
6057 let construction_exists = call
6058 .arguments
6059 .iter()
6060 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6061 if *construction_value {
6062 if construction_exists {
6064 for labeled_arg in &mut call.arguments {
6066 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6067 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6068 value: ast::LiteralValue::Bool(true),
6069 raw: "true".to_string(),
6070 digest: None,
6071 })));
6072 }
6073 }
6074 } else {
6075 call.arguments.push(ast::LabeledArg {
6077 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6078 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6079 value: ast::LiteralValue::Bool(true),
6080 raw: "true".to_string(),
6081 digest: None,
6082 }))),
6083 });
6084 }
6085 } else {
6086 call.arguments
6088 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6089 }
6090 }
6091 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6092 }
6093 }
6094 AstMutateCommand::EditCircle {
6095 start,
6096 center,
6097 construction,
6098 } => {
6099 if let NodeMut::CallExpressionKw(call) = node {
6100 if call.callee.name.name != CIRCLE_FN {
6101 return TraversalReturn::new_continue(());
6102 }
6103 for labeled_arg in &mut call.arguments {
6105 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6106 labeled_arg.arg = start.clone();
6107 }
6108 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6109 labeled_arg.arg = center.clone();
6110 }
6111 }
6112 if let Some(construction_value) = construction {
6114 let construction_exists = call
6115 .arguments
6116 .iter()
6117 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6118 if *construction_value {
6119 if construction_exists {
6120 for labeled_arg in &mut call.arguments {
6122 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6123 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6124 value: ast::LiteralValue::Bool(true),
6125 raw: "true".to_string(),
6126 digest: None,
6127 })));
6128 }
6129 }
6130 } else {
6131 call.arguments.push(ast::LabeledArg {
6133 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6134 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6135 value: ast::LiteralValue::Bool(true),
6136 raw: "true".to_string(),
6137 digest: None,
6138 }))),
6139 });
6140 }
6141 } else {
6142 call.arguments
6144 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6145 }
6146 }
6147 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6148 }
6149 }
6150 AstMutateCommand::EditControlPointSpline { points, construction } => {
6151 if let NodeMut::CallExpressionKw(call) = node {
6152 if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6153 return TraversalReturn::new_continue(());
6154 }
6155 for labeled_arg in &mut call.arguments {
6156 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6157 {
6158 labeled_arg.arg = points.clone();
6159 }
6160 }
6161 if let Some(construction_value) = construction {
6163 let construction_exists = call
6164 .arguments
6165 .iter()
6166 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6167 if *construction_value {
6168 if construction_exists {
6169 for labeled_arg in &mut call.arguments {
6170 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6171 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6172 value: ast::LiteralValue::Bool(true),
6173 raw: "true".to_string(),
6174 digest: None,
6175 })));
6176 }
6177 }
6178 } else {
6179 call.arguments.push(ast::LabeledArg {
6180 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6181 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
6182 value: ast::LiteralValue::Bool(true),
6183 raw: "true".to_string(),
6184 digest: None,
6185 }))),
6186 });
6187 }
6188 } else {
6189 call.arguments
6190 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6191 }
6192 }
6193 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6194 }
6195 }
6196 AstMutateCommand::EditConstraintValue { value } => {
6197 if let NodeMut::BinaryExpression(binary_expr) = node {
6198 let left_is_constraint = matches!(
6199 &binary_expr.left,
6200 ast::BinaryPart::CallExpressionKw(call)
6201 if matches!(
6202 call.callee.name.name.as_str(),
6203 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN | ANGLE_FN
6204 )
6205 );
6206 if left_is_constraint {
6207 binary_expr.right = value.clone();
6208 } else {
6209 binary_expr.left = value.clone();
6210 }
6211
6212 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6213 }
6214 }
6215 AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6216 if let NodeMut::BinaryExpression(binary_expr) = node {
6217 let ast::BinaryPart::CallExpressionKw(call) = &mut binary_expr.left else {
6218 return TraversalReturn::new_continue(());
6219 };
6220 if !matches!(
6221 call.callee.name.name.as_str(),
6222 DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN | RADIUS_FN | DIAMETER_FN
6223 ) {
6224 return TraversalReturn::new_continue(());
6225 }
6226
6227 if let Some(label_arg) = call
6228 .arguments
6229 .iter_mut()
6230 .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6231 {
6232 label_arg.arg = label_position.clone();
6233 } else {
6234 call.arguments.push(ast::LabeledArg {
6235 label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6236 arg: label_position.clone(),
6237 });
6238 }
6239
6240 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6241 }
6242 }
6243 AstMutateCommand::EditCallUnlabeled { arg } => {
6244 if let NodeMut::CallExpressionKw(call) = node {
6245 call.unlabeled = Some(arg.clone());
6246 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6247 }
6248 }
6249 AstMutateCommand::EditVarInitialValue { value } => {
6250 if let NodeMut::SketchVar(sketch_var) = node {
6254 let Ok(literal) = to_source_number(*value) else {
6255 return TraversalReturn::new_break(Err(KclError::refactor(format!(
6256 "Could not convert number to AST literal: {:?}",
6257 *value
6258 ))));
6259 };
6260 sketch_var.initial = Some(Box::new(ast::Node::no_src(literal)));
6261 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6262 }
6263 }
6264 AstMutateCommand::DeleteNode => {
6265 return TraversalReturn {
6266 mutate_body_item: MutateBodyItem::Delete,
6267 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6268 };
6269 }
6270 }
6271 TraversalReturn::new_continue(())
6272}
6273
6274struct FindSketchBlockSourceRange {
6275 target_before_mutation: SourceRange,
6277 found: Cell<Option<AstNodeRef>>,
6281}
6282
6283impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6284 type Error = crate::front::Error;
6285
6286 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6287 let Ok(node_range) = SourceRange::try_from(&node) else {
6288 return Ok(true);
6289 };
6290
6291 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6292 if node_range.module_id() == self.target_before_mutation.module_id()
6293 && node_range.start() == self.target_before_mutation.start()
6294 && node_range.end() >= self.target_before_mutation.end()
6296 {
6297 self.found.set(sketch_block.body.items.last().map(|item| match item {
6298 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6302 _ => AstNodeRef::from(item),
6303 }));
6304 return Ok(false);
6305 } else {
6306 return Ok(true);
6309 }
6310 }
6311
6312 for child in node.children().iter() {
6313 if !child.visit(*self)? {
6314 return Ok(false);
6315 }
6316 }
6317
6318 Ok(true)
6319 }
6320}
6321
6322struct FindSketchBlockByNodePath {
6323 target_node_path: ast::NodePath,
6325 found: Cell<Option<AstNodeRef>>,
6329}
6330
6331impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6332 type Error = crate::front::Error;
6333
6334 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6335 let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6336 return Ok(true);
6337 };
6338
6339 if let crate::walk::Node::SketchBlock(sketch_block) = node {
6340 if let Some(node_path) = node_path
6341 && node_path == self.target_node_path
6342 {
6343 self.found.set(sketch_block.body.items.last().map(|item| match item {
6344 ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6348 _ => AstNodeRef::from(item),
6349 }));
6350
6351 return Ok(false);
6352 } else {
6353 return Ok(true);
6356 }
6357 }
6358
6359 for child in node.children().iter() {
6360 if !child.visit(*self)? {
6361 return Ok(false);
6362 }
6363 }
6364
6365 Ok(true)
6366 }
6367}
6368
6369fn find_sketch_block_added_item(
6377 ast: &ast::Node<ast::Program>,
6378 sketch_block_before_mutation: &AstNodeRef,
6379) -> Result<AstNodeRef, KclError> {
6380 if let Some(node_path) = &sketch_block_before_mutation.node_path {
6381 let find = FindSketchBlockByNodePath {
6382 target_node_path: node_path.clone(),
6383 found: Cell::new(None),
6384 };
6385 let node = crate::walk::Node::from(ast);
6386 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6387 find.found.into_inner().ok_or_else(|| {
6388 KclError::refactor(format!(
6389 "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6390 ))
6391 })
6392 } else {
6393 let find = FindSketchBlockSourceRange {
6395 target_before_mutation: sketch_block_before_mutation.range,
6396 found: Cell::new(None),
6397 };
6398 let node = crate::walk::Node::from(ast);
6399 node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6400 find.found.into_inner().ok_or_else(|| KclError::refactor(
6401 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?"),
6402 ))
6403 }
6404}
6405
6406fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6407 ast.recast_top(&Default::default(), 0)
6409}
6410
6411struct FindNumericLiteral {
6412 target: SourceRange,
6413 found: Cell<Option<ast::NumericLiteral>>,
6414}
6415
6416impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6417 type Error = crate::front::Error;
6418
6419 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6420 let Ok(node_range) = SourceRange::try_from(&node) else {
6421 return Ok(true);
6422 };
6423
6424 if node_range == self.target
6425 && let crate::walk::Node::NumericLiteral(literal) = node
6426 {
6427 self.found.set(Some(literal.inner.clone()));
6428 return Ok(false);
6429 }
6430
6431 for child in node.children().iter() {
6432 if !child.visit(*self)? {
6433 return Ok(false);
6434 }
6435 }
6436
6437 Ok(true)
6438 }
6439}
6440
6441fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6442 let find = FindNumericLiteral {
6443 target,
6444 found: Cell::new(None),
6445 };
6446 let node = crate::walk::Node::from(ast);
6447 node.visit(&find).ok()?;
6448 find.found.into_inner()
6449}
6450
6451struct FindSketchVarInitialByNodePath<'a> {
6452 target: &'a ast::NodePath,
6453 sketch_var_found: Cell<bool>,
6454 initial_literal: Cell<Option<ast::NumericLiteral>>,
6455}
6456
6457impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6458 type Error = crate::front::Error;
6459
6460 fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6461 if let crate::walk::Node::SketchVar(sketch_var) = node
6462 && sketch_var.node_path.as_ref() == Some(self.target)
6463 {
6464 self.sketch_var_found.set(true);
6465 if let Some(initial) = &sketch_var.initial {
6466 self.initial_literal.set(Some(initial.inner.clone()));
6467 }
6468 return Ok(false);
6469 }
6470
6471 for child in node.children().iter() {
6472 if !child.visit(*self)? {
6473 return Ok(false);
6474 }
6475 }
6476
6477 Ok(true)
6478 }
6479}
6480
6481fn numeric_literal_at_node_path(
6491 ast: &ast::Node<ast::Program>,
6492 node_path: Option<&ast::NodePath>,
6493 source_range: SourceRange,
6494) -> Option<Option<ast::NumericLiteral>> {
6495 let Some(node_path) = node_path else {
6496 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";
6497 #[cfg(target_arch = "wasm32")]
6498 web_sys::console::warn_1(&message.into());
6499 #[cfg(not(target_arch = "wasm32"))]
6500 eprintln!("WARNING: {message}");
6501 return numeric_literal_at_source_range(ast, source_range).map(Some);
6502 };
6503 let find = FindSketchVarInitialByNodePath {
6504 target: node_path,
6505 sketch_var_found: Cell::new(false),
6506 initial_literal: Cell::new(None),
6507 };
6508 let node = crate::walk::Node::from(ast);
6509 node.visit(&find).ok()?;
6510 if !find.sketch_var_found.get() {
6511 return None;
6512 }
6513 Some(find.initial_literal.into_inner())
6514}
6515
6516fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6517 match suffix {
6518 NumericSuffix::Mm => Some(UnitLength::Millimeters),
6519 NumericSuffix::Cm => Some(UnitLength::Centimeters),
6520 NumericSuffix::M => Some(UnitLength::Meters),
6521 NumericSuffix::Inch => Some(UnitLength::Inches),
6522 NumericSuffix::Ft => Some(UnitLength::Feet),
6523 NumericSuffix::Yd => Some(UnitLength::Yards),
6524 _ => None,
6525 }
6526}
6527
6528fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6529 match suffix_length_unit(number.units) {
6530 Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6531 None => number.value,
6532 }
6533}
6534
6535fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6536 match suffix_length_unit(literal.suffix) {
6537 Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6538 None => literal.value,
6539 }
6540}
6541
6542fn var_solution_needs_commit(
6543 current_literal: &ast::NumericLiteral,
6544 solved_value: Number,
6545 default_length_unit: UnitLength,
6546) -> bool {
6547 let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6548 let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6549
6550 (current - solved).abs() > 1e-9
6551}
6552
6553fn preserve_var_solution_literal_style(
6554 current_literal: &ast::NumericLiteral,
6555 solved_value: Number,
6556 default_length_unit: UnitLength,
6557) -> Number {
6558 if current_literal.suffix == NumericSuffix::None {
6559 return Number {
6560 value: number_value_in_default_length_units(solved_value, default_length_unit),
6561 units: NumericSuffix::None,
6562 };
6563 }
6564
6565 let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6566 return solved_value;
6567 };
6568
6569 let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6570 Number {
6571 value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6572 units: current_literal.suffix,
6573 }
6574}
6575
6576pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6577 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
6578 inner: ast::ArrayExpression {
6579 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6580 non_code_meta: Default::default(),
6581 digest: None,
6582 },
6583 start: Default::default(),
6584 end: Default::default(),
6585 module_id: Default::default(),
6586 node_path: None,
6587 outer_attrs: Default::default(),
6588 pre_comments: Default::default(),
6589 comment_start: Default::default(),
6590 })))
6591}
6592
6593pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6594 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6595 ast::ArrayExpression {
6596 elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6597 digest: None,
6598 non_code_meta: Default::default(),
6599 },
6600 ))))
6601}
6602
6603fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6604 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
6605 ast::ArrayExpression {
6606 elements: vec![
6607 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6608 point.x,
6609 )?)))),
6610 ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6611 point.y,
6612 )?)))),
6613 ],
6614 non_code_meta: Default::default(),
6615 digest: None,
6616 },
6617 ))))
6618}
6619
6620fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6621 match expr {
6622 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
6623 inner: ast::Literal::from(to_source_number(*number)?),
6624 start: Default::default(),
6625 end: Default::default(),
6626 module_id: Default::default(),
6627 node_path: None,
6628 outer_attrs: Default::default(),
6629 pre_comments: Default::default(),
6630 comment_start: Default::default(),
6631 }))),
6632 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
6633 inner: ast::SketchVar {
6634 initial: Some(Box::new(ast::Node {
6635 inner: to_source_number(*number)?,
6636 start: Default::default(),
6637 end: Default::default(),
6638 module_id: Default::default(),
6639 node_path: None,
6640 outer_attrs: Default::default(),
6641 pre_comments: Default::default(),
6642 comment_start: Default::default(),
6643 })),
6644 digest: None,
6645 },
6646 start: Default::default(),
6647 end: Default::default(),
6648 module_id: Default::default(),
6649 node_path: None,
6650 outer_attrs: Default::default(),
6651 pre_comments: Default::default(),
6652 comment_start: Default::default(),
6653 }))),
6654 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
6655 }
6656}
6657
6658fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
6659 Ok(ast::NumericLiteral {
6660 value: number.value,
6661 suffix: number.units,
6662 raw: format_number_literal(number.value, number.units, None)?,
6663 digest: None,
6664 })
6665}
6666
6667pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
6668 ast::Expr::Name(Box::new(ast_name(name)))
6669}
6670
6671fn ast_name(name: String) -> ast::Node<ast::Name> {
6672 ast::Node {
6673 inner: ast::Name {
6674 name: ast::Node {
6675 inner: ast::Identifier { name, digest: None },
6676 start: Default::default(),
6677 end: Default::default(),
6678 module_id: Default::default(),
6679 node_path: None,
6680 outer_attrs: Default::default(),
6681 pre_comments: Default::default(),
6682 comment_start: Default::default(),
6683 },
6684 path: Vec::new(),
6685 abs_path: false,
6686 digest: None,
6687 },
6688 start: Default::default(),
6689 end: Default::default(),
6690 module_id: Default::default(),
6691 node_path: None,
6692 outer_attrs: Default::default(),
6693 pre_comments: Default::default(),
6694 comment_start: Default::default(),
6695 }
6696}
6697
6698pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
6699 ast::Name {
6700 name: ast::Node {
6701 inner: ast::Identifier {
6702 name: name.to_owned(),
6703 digest: None,
6704 },
6705 start: Default::default(),
6706 end: Default::default(),
6707 module_id: Default::default(),
6708 node_path: None,
6709 outer_attrs: Default::default(),
6710 pre_comments: Default::default(),
6711 comment_start: Default::default(),
6712 },
6713 path: Default::default(),
6714 abs_path: false,
6715 digest: None,
6716 }
6717}
6718
6719pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
6723 let elements = exprs.into_iter().collect::<Vec<_>>();
6724 debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
6725
6726 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6728 elements,
6729 digest: None,
6730 non_code_meta: Default::default(),
6731 })));
6732
6733 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6735 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
6736 unlabeled: Some(array_expr),
6737 arguments: Default::default(),
6738 digest: None,
6739 non_code_meta: Default::default(),
6740 })))
6741}
6742
6743pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
6745 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6746 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
6747 unlabeled: None,
6748 arguments: vec![
6749 ast::LabeledArg {
6750 label: Some(ast::Identifier::new(LINE_START_PARAM)),
6751 arg: start_ast,
6752 },
6753 ast::LabeledArg {
6754 label: Some(ast::Identifier::new(LINE_END_PARAM)),
6755 arg: end_ast,
6756 },
6757 ],
6758 digest: None,
6759 non_code_meta: Default::default(),
6760 })))
6761}
6762
6763pub(crate) fn create_arc_ast(start_ast: ast::Expr, end_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6765 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6766 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
6767 unlabeled: None,
6768 arguments: vec![
6769 ast::LabeledArg {
6770 label: Some(ast::Identifier::new(ARC_START_PARAM)),
6771 arg: start_ast,
6772 },
6773 ast::LabeledArg {
6774 label: Some(ast::Identifier::new(ARC_END_PARAM)),
6775 arg: end_ast,
6776 },
6777 ast::LabeledArg {
6778 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
6779 arg: center_ast,
6780 },
6781 ],
6782 digest: None,
6783 non_code_meta: Default::default(),
6784 })))
6785}
6786
6787pub(crate) fn create_circle_ast(start_ast: ast::Expr, center_ast: ast::Expr) -> ast::Expr {
6789 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6790 callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
6791 unlabeled: None,
6792 arguments: vec![
6793 ast::LabeledArg {
6794 label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
6795 arg: start_ast,
6796 },
6797 ast::LabeledArg {
6798 label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
6799 arg: center_ast,
6800 },
6801 ],
6802 digest: None,
6803 non_code_meta: Default::default(),
6804 })))
6805}
6806
6807pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
6809 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6810 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
6811 unlabeled: Some(line_expr),
6812 arguments: Default::default(),
6813 digest: None,
6814 non_code_meta: Default::default(),
6815 })))
6816}
6817
6818pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
6820 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6821 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
6822 unlabeled: Some(line_expr),
6823 arguments: Default::default(),
6824 digest: None,
6825 non_code_meta: Default::default(),
6826 })))
6827}
6828
6829pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
6831 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6832 object: object_expr,
6833 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
6834 name: ast::Node::no_src(ast::Identifier {
6835 name: property.to_string(),
6836 digest: None,
6837 }),
6838 path: Vec::new(),
6839 abs_path: false,
6840 digest: None,
6841 }))),
6842 computed: false,
6843 digest: None,
6844 })))
6845}
6846
6847pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
6848 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
6849 object: object_expr,
6850 property: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(ast::NumericLiteral {
6851 value: index as f64,
6852 suffix: NumericSuffix::None,
6853 raw: index.to_string(),
6854 digest: None,
6855 })))),
6856 computed: true,
6857 digest: None,
6858 })))
6859}
6860
6861fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
6863 let x_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6865 position.x,
6866 )?))));
6867 let y_literal = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6868 position.y,
6869 )?))));
6870 let point_array = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6871 elements: vec![x_literal, y_literal],
6872 digest: None,
6873 non_code_meta: Default::default(),
6874 })));
6875
6876 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6878 elements: vec![point_expr, point_array],
6879 digest: None,
6880 non_code_meta: Default::default(),
6881 })));
6882
6883 Ok(ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(
6885 ast::CallExpressionKw {
6886 callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
6887 unlabeled: Some(array_expr),
6888 arguments: Default::default(),
6889 digest: None,
6890 non_code_meta: Default::default(),
6891 },
6892 ))))
6893}
6894
6895pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
6897 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6898 elements: line_exprs,
6899 digest: None,
6900 non_code_meta: Default::default(),
6901 })));
6902
6903 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6905 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
6906 unlabeled: Some(array_expr),
6907 arguments: Default::default(),
6908 digest: None,
6909 non_code_meta: Default::default(),
6910 })))
6911}
6912
6913pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
6915 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6916 elements: segment_exprs,
6917 digest: None,
6918 non_code_meta: Default::default(),
6919 })));
6920
6921 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6922 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
6923 unlabeled: Some(array_expr),
6924 arguments: Default::default(),
6925 digest: None,
6926 non_code_meta: Default::default(),
6927 })))
6928}
6929
6930pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
6932 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6933 elements: vec![seg1_expr, seg2_expr],
6934 digest: None,
6935 non_code_meta: Default::default(),
6936 })));
6937
6938 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6939 callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
6940 unlabeled: Some(array_expr),
6941 arguments: Default::default(),
6942 digest: None,
6943 non_code_meta: Default::default(),
6944 })))
6945}
6946
6947pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
6949 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
6950 elements: input_exprs,
6951 digest: None,
6952 non_code_meta: Default::default(),
6953 })));
6954 let arguments = vec![ast::LabeledArg {
6955 label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
6956 arg: axis_expr,
6957 }];
6958
6959 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6960 callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
6961 unlabeled: Some(array_expr),
6962 arguments,
6963 digest: None,
6964 non_code_meta: Default::default(),
6965 })))
6966}
6967
6968pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
6970 let arguments = vec![ast::LabeledArg {
6971 label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
6972 arg: point_expr,
6973 }];
6974
6975 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
6976 callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
6977 unlabeled: Some(segment_expr),
6978 arguments,
6979 digest: None,
6980 non_code_meta: Default::default(),
6981 })))
6982}
6983
6984#[cfg(test)]
6985mod tests {
6986 use std::sync;
6987
6988 use super::*;
6989 use crate::engine::PlaneName;
6990 use crate::engine::engine_manager::EngineManager;
6991 use crate::execution::cache::SketchModeState;
6992 use crate::execution::cache::clear_mem_cache;
6993 use crate::execution::cache::read_old_memory;
6994 use crate::execution::cache::write_old_memory;
6995 use crate::front::Distance;
6996 use crate::front::Fixed;
6997 use crate::front::FixedPoint;
6998 use crate::front::Midpoint;
6999 use crate::front::Object;
7000 use crate::front::Plane;
7001 use crate::front::Sketch;
7002 use crate::front::Tangent;
7003 use crate::frontend::sketch::Vertical;
7004 use crate::pretty::NumericSuffix;
7005
7006 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7007 for object in &scene_graph.objects {
7008 if let ObjectKind::Sketch(_) = &object.kind {
7009 return Some(object);
7010 }
7011 }
7012 None
7013 }
7014
7015 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7016 for object in &scene_graph.objects {
7017 if let ObjectKind::Face(_) = &object.kind {
7018 return Some(object);
7019 }
7020 }
7021 None
7022 }
7023
7024 fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7025 for object in &scene_graph.objects {
7026 if matches!(&object.kind, ObjectKind::Wall(_)) {
7027 return Some(object.id);
7028 }
7029 }
7030 None
7031 }
7032
7033 #[test]
7034 fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7035 let source = "\
7036region001 = region(point = [0.1, 0.1], sketch = s)
7037extrude001 = extrude(region001, length = 5)
7038revolve001 = revolve(region001, axis = Y)
7039sweep001 = sweep(region001, path = path001)
7040loft001 = loft(region001)
7041not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7042";
7043
7044 let program = Program::parse(source).unwrap().0.unwrap();
7045
7046 assert_eq!(
7047 region_name_from_sweep_variable(&program.ast, "extrude001"),
7048 Some("region001".to_owned())
7049 );
7050 assert_eq!(
7051 region_name_from_sweep_variable(&program.ast, "revolve001"),
7052 Some("region001".to_owned())
7053 );
7054 assert_eq!(
7055 region_name_from_sweep_variable(&program.ast, "sweep001"),
7056 Some("region001".to_owned())
7057 );
7058 assert_eq!(
7059 region_name_from_sweep_variable(&program.ast, "loft001"),
7060 Some("region001".to_owned())
7061 );
7062 assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7063 }
7064
7065 #[track_caller]
7066 fn expect_sketch(object: &Object) -> &Sketch {
7067 if let ObjectKind::Sketch(sketch) = &object.kind {
7068 sketch
7069 } else {
7070 panic!("Object is not a sketch: {:?}", object);
7071 }
7072 }
7073
7074 fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7075 let point_object = scene_graph.objects.get(point_id.0).unwrap();
7076 let ObjectKind::Segment {
7077 segment: Segment::Point(point),
7078 } = &point_object.kind
7079 else {
7080 panic!("Object is not a point segment: {point_object:?}");
7081 };
7082 point.position.clone()
7083 }
7084
7085 fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7086 assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7087 assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7088 }
7089
7090 fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7093 Point2d {
7094 x: Expr::Var(Number {
7095 value: x,
7096 units: NumericSuffix::Mm,
7097 }),
7098 y: Expr::Var(Number {
7099 value: y,
7100 units: NumericSuffix::Mm,
7101 }),
7102 }
7103 }
7104
7105 fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7108 Point2d {
7109 x: Number {
7110 value: x,
7111 units: NumericSuffix::Mm,
7112 },
7113 y: Number {
7114 value: y,
7115 units: NumericSuffix::Mm,
7116 },
7117 }
7118 }
7119
7120 fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7121 LineCtor {
7122 start: Point2d {
7123 x: Expr::Number(Number { value: start_x, units }),
7124 y: Expr::Number(Number { value: start_y, units }),
7125 },
7126 end: Point2d {
7127 x: Expr::Number(Number { value: end_x, units }),
7128 y: Expr::Number(Number { value: end_y, units }),
7129 },
7130 construction: None,
7131 }
7132 }
7133
7134 async fn create_sketch_with_single_line(
7135 frontend: &mut FrontendState,
7136 ctx: &ExecutorContext,
7137 mock_ctx: &ExecutorContext,
7138 version: Version,
7139 ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7140 frontend.program = Program::empty();
7141
7142 let sketch_args = SketchCtor {
7143 on: Plane::Default(PlaneName::Xy),
7144 };
7145 let (_src_delta, _scene_delta, sketch_id) = frontend
7146 .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7147 .await
7148 .unwrap();
7149
7150 let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7151 let (source_delta, scene_graph_delta) = frontend
7152 .add_segment(mock_ctx, version, sketch_id, segment, None)
7153 .await
7154 .unwrap();
7155 let line_id = *scene_graph_delta
7156 .new_objects
7157 .last()
7158 .expect("Expected line object id to be created");
7159
7160 (sketch_id, line_id, source_delta, scene_graph_delta)
7161 }
7162
7163 async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7164 frontend.program = program.clone();
7165 let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7166 frontend.update_state_after_exec(outcome, true);
7167 }
7168
7169 #[tokio::test(flavor = "multi_thread")]
7170 async fn test_sketch_checkpoint_round_trip_restores_state() {
7171 let mut frontend = FrontendState::new();
7172 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7173 let mock_ctx = ExecutorContext::new_mock(None).await;
7174 let version = Version(0);
7175
7176 let (sketch_id, line_id, source_delta, scene_graph_delta) =
7177 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7178
7179 let expected_source = source_delta.text.clone();
7180 let expected_scene_graph = frontend.scene_graph.clone();
7181 let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7182 let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7183
7184 let checkpoint_id = frontend
7185 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7186 .await
7187 .unwrap();
7188
7189 let edited_segments = vec![ExistingSegmentCtor {
7190 id: line_id,
7191 ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7192 }];
7193 let (edited_source, _edited_scene) = frontend
7194 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7195 .await
7196 .unwrap();
7197 assert_ne!(edited_source.text, expected_source);
7198
7199 let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7200
7201 assert_eq!(restored.source_delta.text, expected_source);
7202 assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7203 assert!(restored.scene_graph_delta.invalidates_ids);
7204 assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7205 assert_eq!(frontend.scene_graph, expected_scene_graph);
7206 assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7207
7208 ctx.close().await;
7209 }
7210
7211 #[tokio::test(flavor = "multi_thread")]
7212 async fn test_sketch_checkpoints_prune_oldest_entries() {
7213 let mut frontend = FrontendState::new();
7214 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7215 let mock_ctx = ExecutorContext::new_mock(None).await;
7216 let version = Version(0);
7217
7218 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7219 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7220
7221 let mut checkpoint_ids = Vec::new();
7222 for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7223 checkpoint_ids.push(
7224 frontend
7225 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7226 .await
7227 .unwrap(),
7228 );
7229 }
7230
7231 assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7232 assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7233
7234 let oldest_retained = checkpoint_ids[3];
7235 assert_eq!(
7236 frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7237 Some(oldest_retained)
7238 );
7239
7240 let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7241 assert!(evicted_restore.is_err());
7242 assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7243
7244 frontend
7245 .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7246 .await
7247 .unwrap();
7248
7249 ctx.close().await;
7250 }
7251
7252 #[tokio::test(flavor = "multi_thread")]
7253 async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7254 let mut frontend = FrontendState::new();
7255 let missing_checkpoint = SketchCheckpointId::new(999);
7256
7257 let err = frontend
7258 .restore_sketch_checkpoint(missing_checkpoint)
7259 .await
7260 .expect_err("Expected restore to fail for missing checkpoint");
7261
7262 assert!(err.msg.contains("Sketch checkpoint not found"));
7263 }
7264
7265 #[tokio::test(flavor = "multi_thread")]
7266 async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7267 let mut frontend = FrontendState::new();
7268 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7269 let mock_ctx = ExecutorContext::new_mock(None).await;
7270 let version = Version(0);
7271
7272 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7273 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7274
7275 let checkpoint_a = frontend
7276 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7277 .await
7278 .unwrap();
7279 let checkpoint_b = frontend
7280 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7281 .await
7282 .unwrap();
7283 assert_eq!(frontend.sketch_checkpoints.len(), 2);
7284
7285 frontend.clear_sketch_checkpoints();
7286 assert!(frontend.sketch_checkpoints.is_empty());
7287 frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7288 frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7289
7290 ctx.close().await;
7291 }
7292
7293 #[tokio::test(flavor = "multi_thread")]
7294 async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7295 let mut frontend = FrontendState::new();
7296 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7297 let mock_ctx = ExecutorContext::new_mock(None).await;
7298 let version = Version(0);
7299
7300 let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7301 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7302 let old_source = source_delta.text.clone();
7303 let old_checkpoint = frontend
7304 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7305 .await
7306 .unwrap();
7307 let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7308
7309 let new_program = Program::parse("sketch(on = XY) {\n point(at = [1mm, 2mm])\n}\n")
7310 .unwrap()
7311 .0
7312 .unwrap();
7313
7314 let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7315 let SetProgramOutcome::Success {
7316 checkpoint_id: Some(new_checkpoint),
7317 ..
7318 } = result
7319 else {
7320 panic!("Expected Success with a fresh checkpoint baseline");
7321 };
7322
7323 assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7324
7325 let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7326 assert_eq!(old_restore.source_delta.text, old_source);
7327
7328 let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7329 assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7330
7331 ctx.close().await;
7332 }
7333
7334 #[tokio::test(flavor = "multi_thread")]
7335 async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7336 let mut frontend = FrontendState::new();
7337 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7338 let mock_ctx = ExecutorContext::new_mock(None).await;
7339 let version = Version(0);
7340
7341 let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7342 create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7343 let old_checkpoint = frontend
7344 .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7345 .await
7346 .unwrap();
7347 let checkpoint_count_before = frontend.sketch_checkpoints.len();
7348
7349 let failing_program = Program::parse(
7350 "sketch(on = XY) {\n line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7351 )
7352 .unwrap()
7353 .0
7354 .unwrap();
7355
7356 let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7357 assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7358 assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7359 frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7360
7361 ctx.close().await;
7362 }
7363
7364 #[tokio::test(flavor = "multi_thread")]
7365 async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7366 let mut frontend = FrontendState::new();
7367 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7368
7369 let program = Program::parse(
7370 "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",
7371 )
7372 .unwrap()
7373 .0
7374 .unwrap();
7375 let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7376 let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7377 panic!("Expected successful baseline program execution");
7378 };
7379
7380 clear_mem_cache().await;
7381 assert!(read_old_memory().await.is_none());
7382
7383 let checkpoint_without_mock_memory = frontend
7384 .create_sketch_checkpoint((*exec_outcome).clone())
7385 .await
7386 .unwrap();
7387
7388 write_old_memory(SketchModeState::new_for_tests()).await;
7389 assert!(read_old_memory().await.is_some());
7390
7391 let checkpoint_with_mock_memory = frontend
7392 .create_sketch_checkpoint((*exec_outcome).clone())
7393 .await
7394 .unwrap();
7395
7396 clear_mem_cache().await;
7397 assert!(read_old_memory().await.is_none());
7398
7399 frontend
7400 .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7401 .await
7402 .unwrap();
7403 assert!(read_old_memory().await.is_some());
7404
7405 frontend
7406 .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7407 .await
7408 .unwrap();
7409 assert!(read_old_memory().await.is_none());
7410
7411 ctx.close().await;
7412 }
7413
7414 #[tokio::test(flavor = "multi_thread")]
7415 async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7416 let source = "\
7417sketch(on = XY) {
7418 line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7419}
7420
7421bad = missing_name
7422";
7423 let program = Program::parse(source).unwrap().0.unwrap();
7424
7425 let mut frontend = FrontendState::new();
7426
7427 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7428 let mock_ctx = ExecutorContext::new_mock(None).await;
7429 let version = Version(0);
7430 let project_id = ProjectId(0);
7431 let file_id = FileId(0);
7432
7433 let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7434 panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7435 };
7436
7437 let sketch_id = frontend
7438 .scene_graph
7439 .objects
7440 .iter()
7441 .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7442 .expect("Expected sketch object from errored hack_set_program");
7443
7444 frontend
7445 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7446 .await
7447 .unwrap();
7448
7449 ctx.close().await;
7450 mock_ctx.close().await;
7451 }
7452
7453 #[tokio::test(flavor = "multi_thread")]
7454 async fn test_new_sketch_add_point_edit_point() {
7455 let program = Program::empty();
7456
7457 let mut frontend = FrontendState::new();
7458 frontend.program = program;
7459
7460 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7461 let mock_ctx = ExecutorContext::new_mock(None).await;
7462 let version = Version(0);
7463
7464 let sketch_args = SketchCtor {
7465 on: Plane::Default(PlaneName::Xy),
7466 };
7467 let (_src_delta, scene_delta, sketch_id) = frontend
7468 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7469 .await
7470 .unwrap();
7471 assert_eq!(sketch_id, ObjectId(1));
7472 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7473 let sketch_object = &scene_delta.new_graph.objects[1];
7474 assert_eq!(sketch_object.id, ObjectId(1));
7475 assert_eq!(
7476 sketch_object.kind,
7477 ObjectKind::Sketch(Sketch {
7478 args: SketchCtor {
7479 on: Plane::Default(PlaneName::Xy)
7480 },
7481 plane: ObjectId(0),
7482 segments: vec![],
7483 constraints: vec![],
7484 })
7485 );
7486 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7487
7488 let point_ctor = PointCtor {
7489 position: Point2d {
7490 x: Expr::Number(Number {
7491 value: 1.0,
7492 units: NumericSuffix::Inch,
7493 }),
7494 y: Expr::Number(Number {
7495 value: 2.0,
7496 units: NumericSuffix::Inch,
7497 }),
7498 },
7499 };
7500 let segment = SegmentCtor::Point(point_ctor);
7501 let (src_delta, scene_delta) = frontend
7502 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7503 .await
7504 .unwrap();
7505 assert_eq!(
7506 src_delta.text.as_str(),
7507 "sketch001 = sketch(on = XY) {
7508 point(at = [1in, 2in])
7509}
7510"
7511 );
7512 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
7513 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7514 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7515 assert_eq!(scene_object.id.0, i);
7516 }
7517
7518 let point_id = *scene_delta.new_objects.last().unwrap();
7519
7520 let point_ctor = PointCtor {
7521 position: Point2d {
7522 x: Expr::Number(Number {
7523 value: 3.0,
7524 units: NumericSuffix::Inch,
7525 }),
7526 y: Expr::Number(Number {
7527 value: 4.0,
7528 units: NumericSuffix::Inch,
7529 }),
7530 },
7531 };
7532 let segments = vec![ExistingSegmentCtor {
7533 id: point_id,
7534 ctor: SegmentCtor::Point(point_ctor),
7535 }];
7536 let (src_delta, scene_delta) = frontend
7537 .edit_segments(&mock_ctx, version, sketch_id, segments)
7538 .await
7539 .unwrap();
7540 assert_eq!(
7541 src_delta.text.as_str(),
7542 "sketch001 = sketch(on = XY) {
7543 point(at = [3in, 4in])
7544}
7545"
7546 );
7547 assert_eq!(scene_delta.new_objects, vec![]);
7548 assert_eq!(scene_delta.new_graph.objects.len(), 3);
7549
7550 ctx.close().await;
7551 mock_ctx.close().await;
7552 }
7553
7554 #[tokio::test(flavor = "multi_thread")]
7555 async fn test_new_sketch_add_line_edit_line() {
7556 let program = Program::empty();
7557
7558 let mut frontend = FrontendState::new();
7559 frontend.program = program;
7560
7561 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7562 let mock_ctx = ExecutorContext::new_mock(None).await;
7563 let version = Version(0);
7564
7565 let sketch_args = SketchCtor {
7566 on: Plane::Default(PlaneName::Xy),
7567 };
7568 let (_src_delta, scene_delta, sketch_id) = frontend
7569 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7570 .await
7571 .unwrap();
7572 assert_eq!(sketch_id, ObjectId(1));
7573 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7574 let sketch_object = &scene_delta.new_graph.objects[1];
7575 assert_eq!(sketch_object.id, ObjectId(1));
7576 assert_eq!(
7577 sketch_object.kind,
7578 ObjectKind::Sketch(Sketch {
7579 args: SketchCtor {
7580 on: Plane::Default(PlaneName::Xy)
7581 },
7582 plane: ObjectId(0),
7583 segments: vec![],
7584 constraints: vec![],
7585 })
7586 );
7587 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7588
7589 let line_ctor = LineCtor {
7590 start: Point2d {
7591 x: Expr::Number(Number {
7592 value: 0.0,
7593 units: NumericSuffix::Mm,
7594 }),
7595 y: Expr::Number(Number {
7596 value: 0.0,
7597 units: NumericSuffix::Mm,
7598 }),
7599 },
7600 end: Point2d {
7601 x: Expr::Number(Number {
7602 value: 10.0,
7603 units: NumericSuffix::Mm,
7604 }),
7605 y: Expr::Number(Number {
7606 value: 10.0,
7607 units: NumericSuffix::Mm,
7608 }),
7609 },
7610 construction: None,
7611 };
7612 let segment = SegmentCtor::Line(line_ctor);
7613 let (src_delta, scene_delta) = frontend
7614 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7615 .await
7616 .unwrap();
7617 assert_eq!(
7618 src_delta.text.as_str(),
7619 "sketch001 = sketch(on = XY) {
7620 line(start = [0mm, 0mm], end = [10mm, 10mm])
7621}
7622"
7623 );
7624 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7625 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7626 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7627 assert_eq!(scene_object.id.0, i);
7628 }
7629
7630 let line = *scene_delta.new_objects.last().unwrap();
7632
7633 let line_ctor = LineCtor {
7634 start: Point2d {
7635 x: Expr::Number(Number {
7636 value: 1.0,
7637 units: NumericSuffix::Mm,
7638 }),
7639 y: Expr::Number(Number {
7640 value: 2.0,
7641 units: NumericSuffix::Mm,
7642 }),
7643 },
7644 end: Point2d {
7645 x: Expr::Number(Number {
7646 value: 13.0,
7647 units: NumericSuffix::Mm,
7648 }),
7649 y: Expr::Number(Number {
7650 value: 14.0,
7651 units: NumericSuffix::Mm,
7652 }),
7653 },
7654 construction: None,
7655 };
7656 let segments = vec![ExistingSegmentCtor {
7657 id: line,
7658 ctor: SegmentCtor::Line(line_ctor),
7659 }];
7660 let (src_delta, scene_delta) = frontend
7661 .edit_segments(&mock_ctx, version, sketch_id, segments)
7662 .await
7663 .unwrap();
7664 assert_eq!(
7665 src_delta.text.as_str(),
7666 "sketch001 = sketch(on = XY) {
7667 line(start = [1mm, 2mm], end = [13mm, 14mm])
7668}
7669"
7670 );
7671 assert_eq!(scene_delta.new_objects, vec![]);
7672 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7673
7674 ctx.close().await;
7675 mock_ctx.close().await;
7676 }
7677
7678 #[tokio::test(flavor = "multi_thread")]
7679 async fn test_new_sketch_add_arc_edit_arc() {
7680 let program = Program::empty();
7681
7682 let mut frontend = FrontendState::new();
7683 frontend.program = program;
7684
7685 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7686 let mock_ctx = ExecutorContext::new_mock(None).await;
7687 let version = Version(0);
7688
7689 let sketch_args = SketchCtor {
7690 on: Plane::Default(PlaneName::Xy),
7691 };
7692 let (_src_delta, scene_delta, sketch_id) = frontend
7693 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7694 .await
7695 .unwrap();
7696 assert_eq!(sketch_id, ObjectId(1));
7697 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
7698 let sketch_object = &scene_delta.new_graph.objects[1];
7699 assert_eq!(sketch_object.id, ObjectId(1));
7700 assert_eq!(
7701 sketch_object.kind,
7702 ObjectKind::Sketch(Sketch {
7703 args: SketchCtor {
7704 on: Plane::Default(PlaneName::Xy),
7705 },
7706 plane: ObjectId(0),
7707 segments: vec![],
7708 constraints: vec![],
7709 })
7710 );
7711 assert_eq!(scene_delta.new_graph.objects.len(), 2);
7712
7713 let arc_ctor = ArcCtor {
7714 start: Point2d {
7715 x: Expr::Var(Number {
7716 value: 0.0,
7717 units: NumericSuffix::Mm,
7718 }),
7719 y: Expr::Var(Number {
7720 value: 0.0,
7721 units: NumericSuffix::Mm,
7722 }),
7723 },
7724 end: Point2d {
7725 x: Expr::Var(Number {
7726 value: 10.0,
7727 units: NumericSuffix::Mm,
7728 }),
7729 y: Expr::Var(Number {
7730 value: 10.0,
7731 units: NumericSuffix::Mm,
7732 }),
7733 },
7734 center: Point2d {
7735 x: Expr::Var(Number {
7736 value: 10.0,
7737 units: NumericSuffix::Mm,
7738 }),
7739 y: Expr::Var(Number {
7740 value: 0.0,
7741 units: NumericSuffix::Mm,
7742 }),
7743 },
7744 construction: None,
7745 };
7746 let segment = SegmentCtor::Arc(arc_ctor);
7747 let (src_delta, scene_delta) = frontend
7748 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7749 .await
7750 .unwrap();
7751 assert_eq!(
7752 src_delta.text.as_str(),
7753 "sketch001 = sketch(on = XY) {
7754 arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
7755}
7756"
7757 );
7758 assert_eq!(
7759 scene_delta.new_objects,
7760 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
7761 );
7762 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
7763 assert_eq!(scene_object.id.0, i);
7764 }
7765 assert_eq!(scene_delta.new_graph.objects.len(), 6);
7766
7767 let arc = *scene_delta.new_objects.last().unwrap();
7769
7770 let arc_ctor = ArcCtor {
7771 start: Point2d {
7772 x: Expr::Var(Number {
7773 value: 1.0,
7774 units: NumericSuffix::Mm,
7775 }),
7776 y: Expr::Var(Number {
7777 value: 2.0,
7778 units: NumericSuffix::Mm,
7779 }),
7780 },
7781 end: Point2d {
7782 x: Expr::Var(Number {
7783 value: 13.0,
7784 units: NumericSuffix::Mm,
7785 }),
7786 y: Expr::Var(Number {
7787 value: 14.0,
7788 units: NumericSuffix::Mm,
7789 }),
7790 },
7791 center: Point2d {
7792 x: Expr::Var(Number {
7793 value: 13.0,
7794 units: NumericSuffix::Mm,
7795 }),
7796 y: Expr::Var(Number {
7797 value: 2.0,
7798 units: NumericSuffix::Mm,
7799 }),
7800 },
7801 construction: None,
7802 };
7803 let segments = vec![ExistingSegmentCtor {
7804 id: arc,
7805 ctor: SegmentCtor::Arc(arc_ctor),
7806 }];
7807 let (src_delta, scene_delta) = frontend
7808 .edit_segments(&mock_ctx, version, sketch_id, segments)
7809 .await
7810 .unwrap();
7811 assert_eq!(
7812 src_delta.text.as_str(),
7813 "sketch001 = sketch(on = XY) {
7814 arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
7815}
7816"
7817 );
7818 assert_eq!(scene_delta.new_objects, vec![]);
7819 assert_eq!(scene_delta.new_graph.objects.len(), 6);
7820
7821 ctx.close().await;
7822 mock_ctx.close().await;
7823 }
7824
7825 #[tokio::test(flavor = "multi_thread")]
7826 async fn test_new_sketch_add_circle_edit_circle() {
7827 let program = Program::empty();
7828
7829 let mut frontend = FrontendState::new();
7830 frontend.program = program;
7831
7832 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7833 let mock_ctx = ExecutorContext::new_mock(None).await;
7834 let version = Version(0);
7835
7836 let sketch_args = SketchCtor {
7837 on: Plane::Default(PlaneName::Xy),
7838 };
7839 let (_src_delta, _scene_delta, sketch_id) = frontend
7840 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
7841 .await
7842 .unwrap();
7843
7844 let circle_ctor = CircleCtor {
7846 start: Point2d {
7847 x: Expr::Var(Number {
7848 value: 5.0,
7849 units: NumericSuffix::Mm,
7850 }),
7851 y: Expr::Var(Number {
7852 value: 0.0,
7853 units: NumericSuffix::Mm,
7854 }),
7855 },
7856 center: Point2d {
7857 x: Expr::Var(Number {
7858 value: 0.0,
7859 units: NumericSuffix::Mm,
7860 }),
7861 y: Expr::Var(Number {
7862 value: 0.0,
7863 units: NumericSuffix::Mm,
7864 }),
7865 },
7866 construction: None,
7867 };
7868 let segment = SegmentCtor::Circle(circle_ctor);
7869 let (src_delta, scene_delta) = frontend
7870 .add_segment(&mock_ctx, version, sketch_id, segment, None)
7871 .await
7872 .unwrap();
7873 assert_eq!(
7874 src_delta.text.as_str(),
7875 "sketch001 = sketch(on = XY) {
7876 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7877}
7878"
7879 );
7880 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
7882 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7883
7884 let circle = *scene_delta.new_objects.last().unwrap();
7885
7886 let circle_ctor = CircleCtor {
7888 start: Point2d {
7889 x: Expr::Var(Number {
7890 value: 10.0,
7891 units: NumericSuffix::Mm,
7892 }),
7893 y: Expr::Var(Number {
7894 value: 0.0,
7895 units: NumericSuffix::Mm,
7896 }),
7897 },
7898 center: Point2d {
7899 x: Expr::Var(Number {
7900 value: 3.0,
7901 units: NumericSuffix::Mm,
7902 }),
7903 y: Expr::Var(Number {
7904 value: 4.0,
7905 units: NumericSuffix::Mm,
7906 }),
7907 },
7908 construction: None,
7909 };
7910 let segments = vec![ExistingSegmentCtor {
7911 id: circle,
7912 ctor: SegmentCtor::Circle(circle_ctor),
7913 }];
7914 let (src_delta, scene_delta) = frontend
7915 .edit_segments(&mock_ctx, version, sketch_id, segments)
7916 .await
7917 .unwrap();
7918 assert_eq!(
7919 src_delta.text.as_str(),
7920 "sketch001 = sketch(on = XY) {
7921 circle1 = circle(start = [var 10mm, var 0mm], center = [var 3mm, var 4mm])
7922}
7923"
7924 );
7925 assert_eq!(scene_delta.new_objects, vec![]);
7926 assert_eq!(scene_delta.new_graph.objects.len(), 5);
7927
7928 ctx.close().await;
7929 mock_ctx.close().await;
7930 }
7931
7932 #[tokio::test(flavor = "multi_thread")]
7933 async fn test_delete_circle() {
7934 let initial_source = "sketch001 = sketch(on = XY) {
7935 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7936}
7937";
7938
7939 let program = Program::parse(initial_source).unwrap().0.unwrap();
7940 let mut frontend = FrontendState::new();
7941
7942 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7943 let mock_ctx = ExecutorContext::new_mock(None).await;
7944 let version = Version(0);
7945
7946 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7947 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7948 let sketch_id = sketch_object.id;
7949 let sketch = expect_sketch(sketch_object);
7950
7951 assert_eq!(sketch.segments.len(), 3);
7953 let circle_id = sketch.segments[2];
7954
7955 let (src_delta, scene_delta) = frontend
7957 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
7958 .await
7959 .unwrap();
7960 assert_eq!(
7961 src_delta.text.as_str(),
7962 "sketch001 = sketch(on = XY) {
7963}
7964"
7965 );
7966 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
7967 let new_sketch = expect_sketch(new_sketch_object);
7968 assert_eq!(new_sketch.segments.len(), 0);
7969
7970 ctx.close().await;
7971 mock_ctx.close().await;
7972 }
7973
7974 #[tokio::test(flavor = "multi_thread")]
7975 async fn test_edit_circle_via_point() {
7976 let initial_source = "sketch001 = sketch(on = XY) {
7977 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
7978}
7979";
7980
7981 let program = Program::parse(initial_source).unwrap().0.unwrap();
7982 let mut frontend = FrontendState::new();
7983
7984 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
7985 let mock_ctx = ExecutorContext::new_mock(None).await;
7986 let version = Version(0);
7987
7988 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7989 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7990 let sketch_id = sketch_object.id;
7991 let sketch = expect_sketch(sketch_object);
7992
7993 let circle_id = sketch
7995 .segments
7996 .iter()
7997 .copied()
7998 .find(|seg_id| {
7999 matches!(
8000 &frontend.scene_graph.objects[seg_id.0].kind,
8001 ObjectKind::Segment {
8002 segment: Segment::Circle(_)
8003 }
8004 )
8005 })
8006 .expect("Expected a circle segment in sketch");
8007 let circle_object = &frontend.scene_graph.objects[circle_id.0];
8008 let ObjectKind::Segment {
8009 segment: Segment::Circle(circle),
8010 } = &circle_object.kind
8011 else {
8012 panic!("Expected circle segment, got: {:?}", circle_object.kind);
8013 };
8014 let start_point_id = circle.start;
8015
8016 let segments = vec![ExistingSegmentCtor {
8018 id: start_point_id,
8019 ctor: SegmentCtor::Point(PointCtor {
8020 position: Point2d {
8021 x: Expr::Var(Number {
8022 value: 7.0,
8023 units: NumericSuffix::Mm,
8024 }),
8025 y: Expr::Var(Number {
8026 value: 1.0,
8027 units: NumericSuffix::Mm,
8028 }),
8029 },
8030 }),
8031 }];
8032 let (src_delta, _scene_delta) = frontend
8033 .edit_segments(&mock_ctx, version, sketch_id, segments)
8034 .await
8035 .unwrap();
8036 assert_eq!(
8037 src_delta.text.as_str(),
8038 "sketch001 = sketch(on = XY) {
8039 circle(start = [var 7mm, var 1mm], center = [var 0mm, var 0mm])
8040}
8041"
8042 );
8043
8044 ctx.close().await;
8045 mock_ctx.close().await;
8046 }
8047
8048 #[tokio::test(flavor = "multi_thread")]
8049 async fn test_add_line_when_sketch_block_uses_variable() {
8050 let initial_source = "s = sketch(on = XY) {}
8051";
8052
8053 let program = Program::parse(initial_source).unwrap().0.unwrap();
8054
8055 let mut frontend = FrontendState::new();
8056
8057 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8058 let mock_ctx = ExecutorContext::new_mock(None).await;
8059 let version = Version(0);
8060
8061 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8062 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8063 let sketch_id = sketch_object.id;
8064
8065 let line_ctor = LineCtor {
8066 start: Point2d {
8067 x: Expr::Number(Number {
8068 value: 0.0,
8069 units: NumericSuffix::Mm,
8070 }),
8071 y: Expr::Number(Number {
8072 value: 0.0,
8073 units: NumericSuffix::Mm,
8074 }),
8075 },
8076 end: Point2d {
8077 x: Expr::Number(Number {
8078 value: 10.0,
8079 units: NumericSuffix::Mm,
8080 }),
8081 y: Expr::Number(Number {
8082 value: 10.0,
8083 units: NumericSuffix::Mm,
8084 }),
8085 },
8086 construction: None,
8087 };
8088 let segment = SegmentCtor::Line(line_ctor);
8089 let (src_delta, scene_delta) = frontend
8090 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8091 .await
8092 .unwrap();
8093 assert_eq!(
8094 src_delta.text.as_str(),
8095 "s = sketch(on = XY) {
8096 line(start = [0mm, 0mm], end = [10mm, 10mm])
8097}
8098"
8099 );
8100 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8101 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8102
8103 ctx.close().await;
8104 mock_ctx.close().await;
8105 }
8106
8107 #[tokio::test(flavor = "multi_thread")]
8108 async fn test_new_sketch_add_line_delete_sketch() {
8109 let program = Program::empty();
8110
8111 let mut frontend = FrontendState::new();
8112 frontend.program = program;
8113
8114 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8115 let mock_ctx = ExecutorContext::new_mock(None).await;
8116 let version = Version(0);
8117
8118 let sketch_args = SketchCtor {
8119 on: Plane::Default(PlaneName::Xy),
8120 };
8121 let (_src_delta, scene_delta, sketch_id) = frontend
8122 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8123 .await
8124 .unwrap();
8125 assert_eq!(sketch_id, ObjectId(1));
8126 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8127 let sketch_object = &scene_delta.new_graph.objects[1];
8128 assert_eq!(sketch_object.id, ObjectId(1));
8129 assert_eq!(
8130 sketch_object.kind,
8131 ObjectKind::Sketch(Sketch {
8132 args: SketchCtor {
8133 on: Plane::Default(PlaneName::Xy)
8134 },
8135 plane: ObjectId(0),
8136 segments: vec![],
8137 constraints: vec![],
8138 })
8139 );
8140 assert_eq!(scene_delta.new_graph.objects.len(), 2);
8141
8142 let line_ctor = LineCtor {
8143 start: Point2d {
8144 x: Expr::Number(Number {
8145 value: 0.0,
8146 units: NumericSuffix::Mm,
8147 }),
8148 y: Expr::Number(Number {
8149 value: 0.0,
8150 units: NumericSuffix::Mm,
8151 }),
8152 },
8153 end: Point2d {
8154 x: Expr::Number(Number {
8155 value: 10.0,
8156 units: NumericSuffix::Mm,
8157 }),
8158 y: Expr::Number(Number {
8159 value: 10.0,
8160 units: NumericSuffix::Mm,
8161 }),
8162 },
8163 construction: None,
8164 };
8165 let segment = SegmentCtor::Line(line_ctor);
8166 let (src_delta, scene_delta) = frontend
8167 .add_segment(&mock_ctx, version, sketch_id, segment, None)
8168 .await
8169 .unwrap();
8170 assert_eq!(
8171 src_delta.text.as_str(),
8172 "sketch001 = sketch(on = XY) {
8173 line(start = [0mm, 0mm], end = [10mm, 10mm])
8174}
8175"
8176 );
8177 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8178
8179 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8180 assert_eq!(src_delta.text.as_str(), "");
8181 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8182
8183 ctx.close().await;
8184 mock_ctx.close().await;
8185 }
8186
8187 #[tokio::test(flavor = "multi_thread")]
8188 async fn test_delete_sketch_when_sketch_block_uses_variable() {
8189 let initial_source = "s = sketch(on = XY) {}
8190";
8191
8192 let program = Program::parse(initial_source).unwrap().0.unwrap();
8193
8194 let mut frontend = FrontendState::new();
8195
8196 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8197 let version = Version(0);
8198
8199 frontend.hack_set_program(&ctx, program).await.unwrap();
8200 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8201 let sketch_id = sketch_object.id;
8202
8203 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8204 assert_eq!(src_delta.text.as_str(), "");
8205 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8206
8207 ctx.close().await;
8208 }
8209
8210 #[tokio::test(flavor = "multi_thread")]
8211 async fn test_delete_sketch_after_comment() {
8212 let initial_source = "sketch001 = sketch(on = XZ) {
8213}
8214";
8215
8216 let program = Program::parse(initial_source).unwrap().0.unwrap();
8217 let mut frontend = FrontendState::new();
8218
8219 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8220 let version = Version(0);
8221
8222 frontend.hack_set_program(&ctx, program).await.unwrap();
8223 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8224 let sketch_id = sketch_object.id;
8225 let original_source = sketch_object.source.clone();
8226
8227 let commented_source = "// test 1
8228sketch001 = sketch(on = XZ) {
8229}
8230";
8231 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8232 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8233
8234 let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8235 assert_eq!(cached_sketch_object.source, original_source);
8236
8237 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8238 assert!(
8239 !src_delta.text.contains("sketch001"),
8240 "sketch was not deleted: {}",
8241 src_delta.text
8242 );
8243 assert_eq!(src_delta.text.as_str(), "// test 1\n");
8245 assert_eq!(scene_delta.new_graph.objects.len(), 0);
8246
8247 ctx.close().await;
8248 }
8249
8250 #[tokio::test(flavor = "multi_thread")]
8251 async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8252 let initial_source = "sketch001 = sketch(on = XZ) {
8253}
8254foo = 1
8255";
8256
8257 let program = Program::parse(initial_source).unwrap().0.unwrap();
8258 let mut frontend = FrontendState::new();
8259
8260 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8261 let version = Version(0);
8262
8263 frontend.hack_set_program(&ctx, program).await.unwrap();
8264 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8265 let sketch_id = sketch_object.id;
8266
8267 let commented_source = "// keep me
8268sketch001 = sketch(on = XZ) {
8269}
8270foo = 1
8271";
8272 let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8273 frontend.engine_execute(&ctx, commented_program).await.unwrap();
8274
8275 let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8276 assert_eq!(src_delta.text.as_str(), "// keep me\nfoo = 1\n");
8278
8279 ctx.close().await;
8280 }
8281
8282 #[tokio::test(flavor = "multi_thread")]
8283 async fn test_delete_segment_preserves_pre_comment() {
8284 let initial_source = "\
8285sketch(on = XY) {
8286 point(at = [var 1, var 2])
8287 // describe the middle point
8288 point(at = [var 3, var 4])
8289 point(at = [var 5, var 6])
8290}
8291";
8292
8293 let program = Program::parse(initial_source).unwrap().0.unwrap();
8294 let mut frontend = FrontendState::new();
8295
8296 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8297 let mock_ctx = ExecutorContext::new_mock(None).await;
8298 let version = Version(0);
8299
8300 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8301 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8302 let sketch_id = sketch_object.id;
8303 let sketch = expect_sketch(sketch_object);
8304
8305 let middle_point_id = *sketch.segments.get(1).unwrap();
8306
8307 let (src_delta, _scene_delta) = frontend
8308 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8309 .await
8310 .unwrap();
8311 assert_eq!(
8314 src_delta.text.as_str(),
8315 "\
8316sketch(on = XY) {
8317 point(at = [var 1, var 2])
8318 // describe the middle point
8319 point(at = [var 5, var 6])
8320}
8321"
8322 );
8323
8324 ctx.close().await;
8325 mock_ctx.close().await;
8326 }
8327
8328 #[tokio::test(flavor = "multi_thread")]
8329 async fn test_delete_last_segment_preserves_pre_comment() {
8330 let initial_source = "\
8331sketch(on = XY) {
8332 point(at = [var 1, var 2])
8333 // describe the trailing point
8334 point(at = [var 3, var 4])
8335}
8336";
8337
8338 let program = Program::parse(initial_source).unwrap().0.unwrap();
8339 let mut frontend = FrontendState::new();
8340
8341 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8342 let mock_ctx = ExecutorContext::new_mock(None).await;
8343 let version = Version(0);
8344
8345 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8346 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8347 let sketch_id = sketch_object.id;
8348 let sketch = expect_sketch(sketch_object);
8349
8350 let last_point_id = *sketch.segments.last().unwrap();
8351
8352 let (src_delta, _scene_delta) = frontend
8353 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8354 .await
8355 .unwrap();
8356 assert_eq!(
8359 src_delta.text.as_str(),
8360 "\
8361sketch(on = XY) {
8362 point(at = [var 1, var 2])
8363 // describe the trailing point
8364}
8365"
8366 );
8367
8368 ctx.close().await;
8369 mock_ctx.close().await;
8370 }
8371
8372 #[tokio::test(flavor = "multi_thread")]
8373 async fn test_delete_segment_drops_inline_trailing_comment() {
8374 let initial_source = "\
8375sketch(on = XY) {
8376 point(at = [var 1, var 2])
8377 point(at = [var 3, var 4]) // same-line note that gets dropped
8378 point(at = [var 5, var 6])
8379}
8380";
8381
8382 let program = Program::parse(initial_source).unwrap().0.unwrap();
8383 let mut frontend = FrontendState::new();
8384
8385 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8386 let mock_ctx = ExecutorContext::new_mock(None).await;
8387 let version = Version(0);
8388
8389 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8390 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8391 let sketch_id = sketch_object.id;
8392 let sketch = expect_sketch(sketch_object);
8393
8394 let middle_point_id = *sketch.segments.get(1).unwrap();
8395
8396 let (src_delta, _scene_delta) = frontend
8397 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8398 .await
8399 .unwrap();
8400 assert!(
8402 !src_delta.text.contains("same-line note"),
8403 "inline comment should have been removed: {}",
8404 src_delta.text
8405 );
8406
8407 ctx.close().await;
8408 mock_ctx.close().await;
8409 }
8410
8411 #[tokio::test(flavor = "multi_thread")]
8412 async fn test_delete_segments_preserves_block_comments_across_positions() {
8413 let initial_source = "\
8421sketch(on = XY) {
8422 /* above first - moves to middle */
8423 point(at = [var 1, var 2]) /* same-line on first - dropped */
8424 /* above middle - stays */
8425 point(at = [var 3, var 4])
8426 /* above last - moves to trailing meta */
8427 point(at = [var 5, var 6])
8428}
8429";
8430
8431 let program = Program::parse(initial_source).unwrap().0.unwrap();
8432 let mut frontend = FrontendState::new();
8433
8434 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8435 let mock_ctx = ExecutorContext::new_mock(None).await;
8436 let version = Version(0);
8437
8438 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8439 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8440 let sketch_id = sketch_object.id;
8441 let sketch = expect_sketch(sketch_object);
8442
8443 let first_point_id = *sketch.segments.first().unwrap();
8444 let last_point_id = *sketch.segments.last().unwrap();
8445
8446 let (src_delta, _scene_delta) = frontend
8447 .delete_objects(
8448 &mock_ctx,
8449 version,
8450 sketch_id,
8451 Vec::new(),
8452 vec![first_point_id, last_point_id],
8453 )
8454 .await
8455 .unwrap();
8456 assert_eq!(
8457 src_delta.text.as_str(),
8458 "\
8459sketch(on = XY) {
8460 /* above first - moves to middle */
8461 /* above middle - stays */
8462 point(at = [var 3, var 4])
8463 /* above last - moves to trailing meta */
8464}
8465"
8466 );
8467
8468 ctx.close().await;
8469 mock_ctx.close().await;
8470 }
8471
8472 #[tokio::test(flavor = "multi_thread")]
8473 async fn test_edit_line_when_editing_its_start_point() {
8474 let initial_source = "\
8475sketch(on = XY) {
8476 line(start = [var 1, var 2], end = [var 3, var 4])
8477}
8478";
8479
8480 let program = Program::parse(initial_source).unwrap().0.unwrap();
8481
8482 let mut frontend = FrontendState::new();
8483
8484 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8485 let mock_ctx = ExecutorContext::new_mock(None).await;
8486 let version = Version(0);
8487
8488 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8489 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8490 let sketch_id = sketch_object.id;
8491 let sketch = expect_sketch(sketch_object);
8492
8493 let point_id = *sketch.segments.first().unwrap();
8494
8495 let point_ctor = PointCtor {
8496 position: Point2d {
8497 x: Expr::Var(Number {
8498 value: 5.0,
8499 units: NumericSuffix::Inch,
8500 }),
8501 y: Expr::Var(Number {
8502 value: 6.0,
8503 units: NumericSuffix::Inch,
8504 }),
8505 },
8506 };
8507 let segments = vec![ExistingSegmentCtor {
8508 id: point_id,
8509 ctor: SegmentCtor::Point(point_ctor),
8510 }];
8511 let (src_delta, scene_delta) = frontend
8512 .edit_segments(&mock_ctx, version, sketch_id, segments)
8513 .await
8514 .unwrap();
8515 assert_eq!(
8516 src_delta.text.as_str(),
8517 "\
8518sketch(on = XY) {
8519 line(start = [var 5in, var 6in], end = [var 3, var 4])
8520}
8521"
8522 );
8523 assert_eq!(scene_delta.new_objects, vec![]);
8524 assert_eq!(scene_delta.new_graph.objects.len(), 5);
8525
8526 ctx.close().await;
8527 mock_ctx.close().await;
8528 }
8529
8530 #[tokio::test(flavor = "multi_thread")]
8531 async fn test_edit_line_when_editing_its_end_point() {
8532 let initial_source = "\
8533sketch(on = XY) {
8534 line(start = [var 1, var 2], end = [var 3, var 4])
8535}
8536";
8537
8538 let program = Program::parse(initial_source).unwrap().0.unwrap();
8539
8540 let mut frontend = FrontendState::new();
8541
8542 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8543 let mock_ctx = ExecutorContext::new_mock(None).await;
8544 let version = Version(0);
8545
8546 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8547 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8548 let sketch_id = sketch_object.id;
8549 let sketch = expect_sketch(sketch_object);
8550 let point_id = *sketch.segments.get(1).unwrap();
8551
8552 let point_ctor = PointCtor {
8553 position: Point2d {
8554 x: Expr::Var(Number {
8555 value: 5.0,
8556 units: NumericSuffix::Inch,
8557 }),
8558 y: Expr::Var(Number {
8559 value: 6.0,
8560 units: NumericSuffix::Inch,
8561 }),
8562 },
8563 };
8564 let segments = vec![ExistingSegmentCtor {
8565 id: point_id,
8566 ctor: SegmentCtor::Point(point_ctor),
8567 }];
8568 let (src_delta, scene_delta) = frontend
8569 .edit_segments(&mock_ctx, version, sketch_id, segments)
8570 .await
8571 .unwrap();
8572 assert_eq!(
8573 src_delta.text.as_str(),
8574 "\
8575sketch(on = XY) {
8576 line(start = [var 1, var 2], end = [var 5in, var 6in])
8577}
8578"
8579 );
8580 assert_eq!(scene_delta.new_objects, vec![]);
8581 assert_eq!(
8582 scene_delta.new_graph.objects.len(),
8583 5,
8584 "{:#?}",
8585 scene_delta.new_graph.objects
8586 );
8587
8588 ctx.close().await;
8589 mock_ctx.close().await;
8590 }
8591
8592 #[tokio::test(flavor = "multi_thread")]
8593 async fn test_edit_line_with_coincident_feedback() {
8594 let initial_source = "\
8595sketch(on = XY) {
8596 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8597 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8598 fixed([line1.start, [0, 0]])
8599 coincident([line1.end, line2.start])
8600 equalLength([line1, line2])
8601}
8602";
8603
8604 let program = Program::parse(initial_source).unwrap().0.unwrap();
8605
8606 let mut frontend = FrontendState::new();
8607
8608 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
8609 let mock_ctx = ExecutorContext::new_mock(None).await;
8610 let version = Version(0);
8611
8612 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8613 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8614 let sketch_id = sketch_object.id;
8615 let sketch = expect_sketch(sketch_object);
8616 let line2_end_id = *sketch.segments.get(4).unwrap();
8617
8618 let segments = vec![ExistingSegmentCtor {
8619 id: line2_end_id,
8620 ctor: SegmentCtor::Point(PointCtor {
8621 position: Point2d {
8622 x: Expr::Var(Number {
8623 value: 9.0,
8624 units: NumericSuffix::None,
8625 }),
8626 y: Expr::Var(Number {
8627 value: 10.0,
8628 units: NumericSuffix::None,
8629 }),
8630 },
8631 }),
8632 }];
8633 let (src_delta, scene_delta) = frontend
8634 .edit_segments(&mock_ctx, version, sketch_id, segments)
8635 .await
8636 .unwrap();
8637 assert_eq!(
8638 src_delta.text.as_str(),
8639 "\
8640sketch(on = XY) {
8641 line1 = line(start = [var 0, var 0], end = [var 4.14, var 5.32])
8642 line2 = line(start = [var 4.14, var 5.32], end = [var 9, var 10])
8643 fixed([line1.start, [0, 0]])
8644 coincident([line1.end, line2.start])
8645 equalLength([line1, line2])
8646}
8647"
8648 );
8649 assert_eq!(
8650 scene_delta.new_graph.objects.len(),
8651 11,
8652 "{:#?}",
8653 scene_delta.new_graph.objects
8654 );
8655
8656 ctx.close().await;
8657 mock_ctx.close().await;
8658 }
8659
8660 #[tokio::test(flavor = "multi_thread")]
8661 async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
8662 let initial_source = "\
8663sketch(on = XY) {
8664 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8665 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8666 fixed([line1.start, [0, 0]])
8667 coincident([line1.end, line2.start])
8668 equalLength([line1, line2])
8669}
8670";
8671
8672 let program = Program::parse(initial_source).unwrap().0.unwrap();
8673 let mut frontend = FrontendState::new();
8674 let mock_ctx = ExecutorContext::new_mock(None).await;
8675 let version = Version(0);
8676
8677 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8678 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8679 let sketch_id = sketch_object.id;
8680 let sketch = expect_sketch(sketch_object);
8681 let line2_end_id = *sketch.segments.get(4).unwrap();
8682
8683 let segments = vec![ExistingSegmentCtor {
8684 id: line2_end_id,
8685 ctor: SegmentCtor::Point(PointCtor {
8686 position: Point2d {
8687 x: Expr::Var(Number {
8688 value: 9.0,
8689 units: NumericSuffix::None,
8690 }),
8691 y: Expr::Var(Number {
8692 value: 10.0,
8693 units: NumericSuffix::None,
8694 }),
8695 },
8696 }),
8697 }];
8698 let (edited_source, _) = frontend
8699 .edit_segments(&mock_ctx, version, sketch_id, segments)
8700 .await
8701 .unwrap();
8702
8703 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8704 assert_eq!(mock_source.text, edited_source.text);
8705
8706 mock_ctx.close().await;
8707 }
8708
8709 #[tokio::test(flavor = "multi_thread")]
8712 async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
8713 let initial_source = "\
8714sketch(on = XY) {
8715 line1 = line(start = [var 1, var 2], end = [var 1, var 2])
8716 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
8717 fixed([line1.start, [0, 0]])
8718 coincident([line1.end, line2.start])
8719 equalLength([line1, line2])
8720}
8721";
8722
8723 let program = Program::parse(initial_source).unwrap().0.unwrap();
8724 let mut frontend = FrontendState::new();
8725 let mock_ctx = ExecutorContext::new_mock(None).await;
8726 let version = Version(0);
8727
8728 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8729 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8730 let sketch_id = sketch_object.id;
8731 let sketch = expect_sketch(sketch_object);
8732 let line2_end_id = *sketch.segments.get(4).unwrap();
8733
8734 let segments = vec![ExistingSegmentCtor {
8735 id: line2_end_id,
8736 ctor: SegmentCtor::Point(PointCtor {
8737 position: Point2d {
8738 x: Expr::Var(Number {
8739 value: 9.0,
8740 units: NumericSuffix::None,
8741 }),
8742 y: Expr::Var(Number {
8743 value: 10.0,
8744 units: NumericSuffix::None,
8745 }),
8746 },
8747 }),
8748 }];
8749 let (preview_source, preview_delta) = frontend
8750 .edit_segments_with_options(
8751 &mock_ctx,
8752 version,
8753 sketch_id,
8754 segments,
8755 EditSegmentsOptions {
8756 anchor_segment_ids: Some(vec![line2_end_id]),
8757 drag_anchors: Vec::new(),
8758 commit_solved_initial_guesses: false,
8759 },
8760 )
8761 .await
8762 .unwrap();
8763
8764 assert!(
8765 !preview_delta.exec_outcome.var_solutions.is_empty(),
8766 "preview solve should still solve and return geometry feedback"
8767 );
8768 assert!(
8769 preview_source
8770 .text
8771 .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
8772 );
8773 assert!(
8774 preview_source
8775 .text
8776 .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
8777 );
8778
8779 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8780 assert_eq!(mock_source.text, preview_source.text);
8781
8782 mock_ctx.close().await;
8783 }
8784
8785 #[tokio::test(flavor = "multi_thread")]
8786 async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
8787 let initial_source = "\
8788sketch(on = XY) {
8789 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
8790}
8791";
8792
8793 let program = Program::parse(initial_source).unwrap().0.unwrap();
8794 let mut frontend = FrontendState::new();
8795 let mock_ctx = ExecutorContext::new_mock(None).await;
8796 let version = Version(0);
8797
8798 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8799 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8800 let sketch_id = sketch_object.id;
8801 let sketch = expect_sketch(sketch_object);
8802 let line_end_id = *sketch.segments.get(1).unwrap();
8803
8804 let constraint = Constraint::Fixed(Fixed {
8805 points: vec![FixedPoint {
8806 point: line_end_id,
8807 position: Point2d {
8808 x: Number {
8809 value: 20.0,
8810 units: NumericSuffix::Mm,
8811 },
8812 y: Number {
8813 value: 0.0,
8814 units: NumericSuffix::Mm,
8815 },
8816 },
8817 }],
8818 });
8819 let (constraint_source, _) = frontend
8820 .add_constraint(&mock_ctx, version, sketch_id, constraint)
8821 .await
8822 .unwrap();
8823
8824 assert!(
8825 constraint_source
8826 .text
8827 .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
8828 "{}",
8829 constraint_source.text
8830 );
8831 let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
8832 assert_eq!(mock_source.text, constraint_source.text);
8833
8834 mock_ctx.close().await;
8835 }
8836
8837 #[test]
8838 fn test_no_solver_feedback_preserves_original_source() {
8839 let initial_source = "\
8840@settings(defaultLengthUnit = in, kclVersion = 2.0)
8841cylinder = startSketchOn(XY)
8842 |> circle(center= [0, 0], radius= 22)
8843 |> extrude(length = 14)
8844";
8845 let mut frontend = FrontendState::new();
8846 frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
8847 let outcome = ExecOutcome {
8848 variables: Default::default(),
8849 operations: Default::default(),
8850 artifact_graph: Default::default(),
8851 scene_objects: Default::default(),
8852 source_range_to_object: Default::default(),
8853 var_solutions: Default::default(),
8854 issues: Default::default(),
8855 filenames: Default::default(),
8856 default_planes: Default::default(),
8857 };
8858
8859 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
8860
8861 assert_eq!(source_delta.text, initial_source);
8862 }
8863
8864 #[tokio::test(flavor = "multi_thread")]
8867 async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
8868 let initial_source = "\
8869sketch(on = XY) {
8870 point1 = point(at = [var 0mm, var 0mm])
8871 point2 = point(at = [var 0mm, var 0mm])
8872 coincident([point1, point2])
8873}
8874";
8875
8876 let program = Program::parse(initial_source).unwrap().0.unwrap();
8877 let mut frontend = FrontendState::new();
8878 let mock_ctx = ExecutorContext::new_mock(None).await;
8879 let version = Version(0);
8880
8881 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8882 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8883 let sketch_id = sketch_object.id;
8884 let sketch = expect_sketch(sketch_object);
8885 let point1_id = sketch.segments[0];
8886 let point2_id = sketch.segments[1];
8887
8888 let segments = vec![
8889 ExistingSegmentCtor {
8890 id: point1_id,
8891 ctor: SegmentCtor::Point(PointCtor {
8892 position: point_expr_mm(10.0, 0.0),
8893 }),
8894 },
8895 ExistingSegmentCtor {
8896 id: point2_id,
8897 ctor: SegmentCtor::Point(PointCtor {
8898 position: point_expr_mm(100.0, 0.0),
8899 }),
8900 },
8901 ];
8902 let (_, scene_delta) = frontend
8903 .edit_segments_with_options(
8904 &mock_ctx,
8905 version,
8906 sketch_id,
8907 segments,
8908 EditSegmentsOptions {
8909 anchor_segment_ids: Some(vec![point1_id]),
8910 drag_anchors: Vec::new(),
8911 commit_solved_initial_guesses: true,
8912 },
8913 )
8914 .await
8915 .unwrap();
8916
8917 assert_point_position_close(
8918 point_position(&scene_delta.new_graph, point1_id),
8919 point_number_mm(10.0, 0.0),
8920 );
8921 assert_point_position_close(
8922 point_position(&scene_delta.new_graph, point2_id),
8923 point_number_mm(10.0, 0.0),
8924 );
8925
8926 mock_ctx.close().await;
8927 }
8928
8929 fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
8932 use std::cell::RefCell;
8933 struct Collector {
8934 target: f64,
8935 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
8936 }
8937 impl<'a> crate::walk::Visitor<'a> for &Collector {
8938 type Error = crate::front::Error;
8939 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
8940 if let crate::walk::Node::SketchVar(sketch_var) = node
8941 && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
8942 && (initial.value - self.target).abs() < 1e-9
8943 {
8944 self.out
8945 .borrow_mut()
8946 .push((SourceRange::from(initial.as_ref()), node_path.clone()));
8947 }
8948 for child in node.children().iter() {
8949 if !child.visit(*self)? {
8950 return Ok(false);
8951 }
8952 }
8953 Ok(true)
8954 }
8955 }
8956 let collector = Collector {
8957 target: value,
8958 out: Default::default(),
8959 };
8960 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
8961 collector.out.into_inner()
8962 }
8963
8964 fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
8967 use std::cell::RefCell;
8968 struct Collector {
8969 out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
8970 }
8971 impl<'a> crate::walk::Visitor<'a> for &Collector {
8972 type Error = crate::front::Error;
8973 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
8974 if let crate::walk::Node::SketchVar(sketch_var) = node
8975 && let Some(node_path) = &sketch_var.node_path
8976 {
8977 self.out
8978 .borrow_mut()
8979 .push((SourceRange::from(sketch_var), node_path.clone()));
8980 }
8981 for child in node.children().iter() {
8982 if !child.visit(*self)? {
8983 return Ok(false);
8984 }
8985 }
8986 Ok(true)
8987 }
8988 }
8989 let collector = Collector {
8990 out: Default::default(),
8991 };
8992 let _ = crate::walk::Node::from(&program.ast).visit(&collector);
8993 collector.out.into_inner()
8994 }
8995
8996 fn empty_exec_outcome_with_var_solutions(
8997 var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
8998 ) -> ExecOutcome {
8999 ExecOutcome {
9000 variables: Default::default(),
9001 operations: Default::default(),
9002 artifact_graph: Default::default(),
9003 scene_objects: Default::default(),
9004 source_range_to_object: Default::default(),
9005 var_solutions,
9006 issues: Default::default(),
9007 filenames: Default::default(),
9008 default_planes: Default::default(),
9009 }
9010 }
9011
9012 #[test]
9016 fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9017 let initial_source = "\
9018sketch(on = XY) {
9019 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9020}
9021";
9022 let program = Program::parse(initial_source).unwrap().0.unwrap();
9023 let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9024 assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9025 let (literal_range, node_path) = matches.into_iter().next().unwrap();
9026
9027 let mut frontend = FrontendState::new();
9028 frontend.program = program;
9029
9030 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9031 literal_range,
9032 Some(node_path),
9033 Number {
9034 value: 25.0,
9035 units: NumericSuffix::Mm,
9036 },
9037 )]);
9038
9039 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9040
9041 assert_eq!(
9042 source_delta.text,
9043 "\
9044sketch(on = XY) {
9045 line1 = line(start = [var 0, var 0], end = [var 25mm, var 0])
9046}
9047",
9048 );
9049 }
9050
9051 #[test]
9058 fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9059 let compact_source = "\
9060sketch(on = XY) {
9061 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9062}
9063";
9064 let padded_source = "\
9065// added comment\n// added comment\n\nsketch(on = XY) {
9066 line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9067}
9068";
9069 let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9070 let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9071
9072 let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9073 .into_iter()
9074 .next()
9075 .expect("expected `var 10mm` in compact source");
9076 let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9077 .into_iter()
9078 .next()
9079 .expect("expected `var 10mm` in padded source");
9080
9081 assert_ne!(
9082 compact_match.0, padded_match.0,
9083 "byte offsets must differ for this test to be meaningful"
9084 );
9085 assert_eq!(
9086 compact_match.1, padded_match.1,
9087 "node paths must agree across whitespace; that's the whole point of NodePath",
9088 );
9089
9090 let mut frontend = FrontendState::new();
9091 frontend.program = padded_program;
9092
9093 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9095 compact_match.0,
9096 Some(compact_match.1),
9097 Number {
9098 value: 30.0,
9099 units: NumericSuffix::Mm,
9100 },
9101 )]);
9102
9103 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9104
9105 assert_eq!(
9106 source_delta.text,
9107 "\
9108// added comment
9109// added comment
9110
9111sketch(on = XY) {
9112 line1 = line(start = [var 0, var 0], end = [var 30mm, var 0])
9113}
9114",
9115 );
9116 }
9117
9118 #[test]
9122 fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9123 let initial_source = "\
9124sketch(on = XY) {
9125 line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9126}
9127";
9128 let program = Program::parse(initial_source).unwrap().0.unwrap();
9129
9130 let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9131 .into_iter()
9132 .next()
9133 .expect("expected `var 10mm`");
9134 let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9135 .into_iter()
9136 .next()
9137 .expect("expected `var 20mm`");
9138
9139 let mut frontend = FrontendState::new();
9140 frontend.program = program;
9141
9142 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9144 var_20.0,
9145 Some(var_10.1),
9146 Number {
9147 value: 33.0,
9148 units: NumericSuffix::Mm,
9149 },
9150 )]);
9151
9152 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9153
9154 assert_eq!(
9155 source_delta.text,
9156 "\
9157sketch(on = XY) {
9158 line1 = line(start = [var 33mm, var 0mm], end = [var 20mm, var 0mm])
9159}
9160",
9161 );
9162 }
9163
9164 #[test]
9171 fn test_commit_var_solution_writes_back_into_bare_var() {
9172 let initial_source = "\
9173@settings(experimentalFeatures = allow, kclVersion = 2.0)
9174sketch(on = XY) {
9175 line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9176}
9177";
9178 let program = Program::parse(initial_source).unwrap().0.unwrap();
9179
9180 let bare = collect_all_sketch_vars(&program)
9183 .into_iter()
9184 .find(|(range, _)| {
9185 range.end() - range.start() == 3
9187 })
9188 .expect("expected at least one bare `var`");
9189
9190 let mut frontend = FrontendState::new();
9191 frontend.program = program;
9192
9193 let outcome = empty_exec_outcome_with_var_solutions(vec![(
9194 bare.0,
9195 Some(bare.1),
9196 Number {
9197 value: 7.0,
9198 units: NumericSuffix::Mm,
9199 },
9200 )]);
9201
9202 let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9203
9204 assert_eq!(
9208 source_delta.text,
9209 "\
9210@settings(experimentalFeatures = allow, kclVersion = 2.0)
9211
9212sketch(on = XY) {
9213 line1 = line(start = [var 7mm, var 0mm], end = [var 10mm, var 0])
9214}
9215",
9216 );
9217 }
9218
9219 #[tokio::test(flavor = "multi_thread")]
9220 async fn test_delete_point_without_var() {
9221 let initial_source = "\
9222sketch(on = XY) {
9223 point(at = [var 1, var 2])
9224 point(at = [var 3, var 4])
9225 point(at = [var 5, var 6])
9226}
9227";
9228
9229 let program = Program::parse(initial_source).unwrap().0.unwrap();
9230
9231 let mut frontend = FrontendState::new();
9232
9233 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9234 let mock_ctx = ExecutorContext::new_mock(None).await;
9235 let version = Version(0);
9236
9237 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9238 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9239 let sketch_id = sketch_object.id;
9240 let sketch = expect_sketch(sketch_object);
9241
9242 let point_id = *sketch.segments.get(1).unwrap();
9243
9244 let (src_delta, scene_delta) = frontend
9245 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9246 .await
9247 .unwrap();
9248 assert_eq!(
9249 src_delta.text.as_str(),
9250 "\
9251sketch(on = XY) {
9252 point(at = [var 1, var 2])
9253 point(at = [var 5, var 6])
9254}
9255"
9256 );
9257 assert_eq!(scene_delta.new_objects, vec![]);
9258 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9259
9260 ctx.close().await;
9261 mock_ctx.close().await;
9262 }
9263
9264 #[tokio::test(flavor = "multi_thread")]
9265 async fn test_delete_point_with_var() {
9266 let initial_source = "\
9267sketch(on = XY) {
9268 point(at = [var 1, var 2])
9269 point1 = point(at = [var 3, var 4])
9270 point(at = [var 5, var 6])
9271}
9272";
9273
9274 let program = Program::parse(initial_source).unwrap().0.unwrap();
9275
9276 let mut frontend = FrontendState::new();
9277
9278 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9279 let mock_ctx = ExecutorContext::new_mock(None).await;
9280 let version = Version(0);
9281
9282 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9283 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9284 let sketch_id = sketch_object.id;
9285 let sketch = expect_sketch(sketch_object);
9286
9287 let point_id = *sketch.segments.get(1).unwrap();
9288
9289 let (src_delta, scene_delta) = frontend
9290 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9291 .await
9292 .unwrap();
9293 assert_eq!(
9294 src_delta.text.as_str(),
9295 "\
9296sketch(on = XY) {
9297 point(at = [var 1, var 2])
9298 point(at = [var 5, var 6])
9299}
9300"
9301 );
9302 assert_eq!(scene_delta.new_objects, vec![]);
9303 assert_eq!(scene_delta.new_graph.objects.len(), 4);
9304
9305 ctx.close().await;
9306 mock_ctx.close().await;
9307 }
9308
9309 #[tokio::test(flavor = "multi_thread")]
9310 async fn test_delete_multiple_points() {
9311 let initial_source = "\
9312sketch(on = XY) {
9313 point(at = [var 1, var 2])
9314 point1 = point(at = [var 3, var 4])
9315 point(at = [var 5, var 6])
9316}
9317";
9318
9319 let program = Program::parse(initial_source).unwrap().0.unwrap();
9320
9321 let mut frontend = FrontendState::new();
9322
9323 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9324 let mock_ctx = ExecutorContext::new_mock(None).await;
9325 let version = Version(0);
9326
9327 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9328 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9329 let sketch_id = sketch_object.id;
9330
9331 let sketch = expect_sketch(sketch_object);
9332
9333 let point1_id = *sketch.segments.first().unwrap();
9334 let point2_id = *sketch.segments.get(1).unwrap();
9335
9336 let (src_delta, scene_delta) = frontend
9337 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9338 .await
9339 .unwrap();
9340 assert_eq!(
9341 src_delta.text.as_str(),
9342 "\
9343sketch(on = XY) {
9344 point(at = [var 5, var 6])
9345}
9346"
9347 );
9348 assert_eq!(scene_delta.new_objects, vec![]);
9349 assert_eq!(scene_delta.new_graph.objects.len(), 3);
9350
9351 ctx.close().await;
9352 mock_ctx.close().await;
9353 }
9354
9355 #[tokio::test(flavor = "multi_thread")]
9356 async fn test_delete_coincident_constraint() {
9357 let initial_source = "\
9358sketch(on = XY) {
9359 point1 = point(at = [var 1, var 2])
9360 point2 = point(at = [var 3, var 4])
9361 coincident([point1, point2])
9362 point(at = [var 5, var 6])
9363}
9364";
9365
9366 let program = Program::parse(initial_source).unwrap().0.unwrap();
9367
9368 let mut frontend = FrontendState::new();
9369
9370 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9371 let mock_ctx = ExecutorContext::new_mock(None).await;
9372 let version = Version(0);
9373
9374 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9375 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9376 let sketch_id = sketch_object.id;
9377 let sketch = expect_sketch(sketch_object);
9378
9379 let coincident_id = *sketch.constraints.first().unwrap();
9380
9381 let (src_delta, scene_delta) = frontend
9382 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9383 .await
9384 .unwrap();
9385 assert_eq!(
9386 src_delta.text.as_str(),
9387 "\
9388sketch(on = XY) {
9389 point1 = point(at = [var 1, var 2])
9390 point2 = point(at = [var 3, var 4])
9391 point(at = [var 5, var 6])
9392}
9393"
9394 );
9395 assert_eq!(scene_delta.new_objects, vec![]);
9396 assert_eq!(scene_delta.new_graph.objects.len(), 5);
9397
9398 ctx.close().await;
9399 mock_ctx.close().await;
9400 }
9401
9402 #[tokio::test(flavor = "multi_thread")]
9403 async fn test_delete_line_cascades_to_coincident_constraint() {
9404 let initial_source = "\
9405sketch(on = XY) {
9406 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9407 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9408 coincident([line1.end, line2.start])
9409}
9410";
9411
9412 let program = Program::parse(initial_source).unwrap().0.unwrap();
9413
9414 let mut frontend = FrontendState::new();
9415
9416 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9417 let mock_ctx = ExecutorContext::new_mock(None).await;
9418 let version = Version(0);
9419
9420 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9421 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9422 let sketch_id = sketch_object.id;
9423 let sketch = expect_sketch(sketch_object);
9424 let line_id = *sketch.segments.get(5).unwrap();
9425
9426 let (src_delta, scene_delta) = frontend
9427 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9428 .await
9429 .unwrap();
9430 assert_eq!(
9431 src_delta.text.as_str(),
9432 "\
9433sketch(on = XY) {
9434 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9435}
9436"
9437 );
9438 assert_eq!(
9439 scene_delta.new_graph.objects.len(),
9440 5,
9441 "{:#?}",
9442 scene_delta.new_graph.objects
9443 );
9444
9445 ctx.close().await;
9446 mock_ctx.close().await;
9447 }
9448
9449 #[tokio::test(flavor = "multi_thread")]
9450 async fn test_delete_line_cascades_to_distance_constraint() {
9451 let initial_source = "\
9452sketch(on = XY) {
9453 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9454 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9455 distance([line1.end, line2.start]) == 10mm
9456}
9457";
9458
9459 let program = Program::parse(initial_source).unwrap().0.unwrap();
9460
9461 let mut frontend = FrontendState::new();
9462
9463 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9464 let mock_ctx = ExecutorContext::new_mock(None).await;
9465 let version = Version(0);
9466
9467 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9468 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9469 let sketch_id = sketch_object.id;
9470 let sketch = expect_sketch(sketch_object);
9471 let line_id = *sketch.segments.get(5).unwrap();
9472
9473 let (src_delta, scene_delta) = frontend
9474 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9475 .await
9476 .unwrap();
9477 assert_eq!(
9478 src_delta.text.as_str(),
9479 "\
9480sketch(on = XY) {
9481 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9482}
9483"
9484 );
9485 assert_eq!(
9486 scene_delta.new_graph.objects.len(),
9487 5,
9488 "{:#?}",
9489 scene_delta.new_graph.objects
9490 );
9491
9492 ctx.close().await;
9493 mock_ctx.close().await;
9494 }
9495
9496 #[tokio::test(flavor = "multi_thread")]
9497 async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9498 let initial_source = "\
9499sketch(on = XY) {
9500 point1 = point(at = [var 1, var 2])
9501 point2 = point(at = [var 3, var 4])
9502 horizontalDistance([point1, point2]) == 10mm
9503}
9504";
9505
9506 let program = Program::parse(initial_source).unwrap().0.unwrap();
9507
9508 let mut frontend = FrontendState::new();
9509
9510 let mock_ctx = ExecutorContext::new_mock(None).await;
9511 let version = Version(0);
9512
9513 frontend.program = program.clone();
9514 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9515 frontend.update_state_after_exec(outcome, true);
9516 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9517 let sketch_id = sketch_object.id;
9518 let sketch = expect_sketch(sketch_object);
9519 let point2_id = *sketch.segments.get(1).unwrap();
9520
9521 let (src_delta, scene_delta) = frontend
9522 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9523 .await
9524 .unwrap();
9525 assert_eq!(
9526 src_delta.text.as_str(),
9527 "\
9528sketch(on = XY) {
9529 point1 = point(at = [var 1, var 2])
9530}
9531"
9532 );
9533 assert_eq!(
9534 scene_delta.new_graph.objects.len(),
9535 3,
9536 "{:#?}",
9537 scene_delta.new_graph.objects
9538 );
9539
9540 mock_ctx.close().await;
9541 }
9542
9543 #[tokio::test(flavor = "multi_thread")]
9544 async fn test_delete_line_cascades_to_fixed_constraint() {
9545 let initial_source = "\
9546sketch(on = XY) {
9547 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9548 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9549 fixed([line1.start, [0, 0]])
9550}
9551";
9552
9553 let program = Program::parse(initial_source).unwrap().0.unwrap();
9554
9555 let mut frontend = FrontendState::new();
9556
9557 let mock_ctx = ExecutorContext::new_mock(None).await;
9558 let version = Version(0);
9559
9560 frontend.program = program.clone();
9561 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9562 frontend.update_state_after_exec(outcome, true);
9563 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9564 let sketch_id = sketch_object.id;
9565 let sketch = expect_sketch(sketch_object);
9566 let line1_id = *sketch.segments.get(2).unwrap();
9567
9568 let (src_delta, scene_delta) = frontend
9569 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9570 .await
9571 .unwrap();
9572 assert_eq!(
9573 src_delta.text.as_str(),
9574 "\
9575sketch(on = XY) {
9576 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9577}
9578"
9579 );
9580 assert_eq!(
9581 scene_delta.new_graph.objects.len(),
9582 5,
9583 "{:#?}",
9584 scene_delta.new_graph.objects
9585 );
9586
9587 mock_ctx.close().await;
9588 }
9589
9590 #[tokio::test(flavor = "multi_thread")]
9591 async fn test_delete_line_cascades_to_midpoint_constraint() {
9592 let initial_source = "\
9593sketch(on = XY) {
9594 point1 = point(at = [var 1, var 2])
9595 line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9596 midpoint(line1, point = point1)
9597}
9598";
9599
9600 let program = Program::parse(initial_source).unwrap().0.unwrap();
9601
9602 let mut frontend = FrontendState::new();
9603
9604 let mock_ctx = ExecutorContext::new_mock(None).await;
9605 let version = Version(0);
9606
9607 frontend.program = program.clone();
9608 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9609 frontend.update_state_after_exec(outcome, true);
9610 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9611 let sketch_id = sketch_object.id;
9612 let sketch = expect_sketch(sketch_object);
9613 let line1_id = *sketch.segments.get(3).unwrap();
9614
9615 let (src_delta, scene_delta) = frontend
9616 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9617 .await
9618 .unwrap();
9619 assert_eq!(
9620 src_delta.text.as_str(),
9621 "\
9622sketch(on = XY) {
9623 point1 = point(at = [var 1, var 2])
9624}
9625"
9626 );
9627 assert_eq!(
9628 scene_delta.new_graph.objects.len(),
9629 3,
9630 "{:#?}",
9631 scene_delta.new_graph.objects
9632 );
9633
9634 mock_ctx.close().await;
9635 }
9636
9637 #[tokio::test(flavor = "multi_thread")]
9638 async fn test_delete_point_preserves_multiline_coincident_constraint() {
9639 let initial_source = "\
9640sketch(on = XY) {
9641 point1 = point(at = [var 1, var 2])
9642 point2 = point(at = [var 3, var 4])
9643 point3 = point(at = [var 5, var 6])
9644 coincident([point1, point2, point3])
9645}
9646";
9647
9648 let program = Program::parse(initial_source).unwrap().0.unwrap();
9649
9650 let mut frontend = FrontendState::new();
9651
9652 let mock_ctx = ExecutorContext::new_mock(None).await;
9653 let version = Version(0);
9654
9655 frontend.program = program.clone();
9656 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9657 frontend.update_state_after_exec(outcome, true);
9658 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9659 let sketch_id = sketch_object.id;
9660 let sketch = expect_sketch(sketch_object);
9661 let point3_id = *sketch.segments.get(2).unwrap();
9662
9663 let (src_delta, scene_delta) = frontend
9664 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
9665 .await
9666 .unwrap();
9667 assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
9668 assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
9669 assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
9670 assert!(
9671 src_delta.text.contains("coincident([point1, point2])"),
9672 "{}",
9673 src_delta.text
9674 );
9675
9676 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9677 let sketch = expect_sketch(sketch_object);
9678 assert_eq!(sketch.segments.len(), 2);
9679 assert_eq!(sketch.constraints.len(), 1);
9680
9681 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9682 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9683 panic!("Expected constraint object");
9684 };
9685 let Constraint::Coincident(coincident) = constraint else {
9686 panic!("Expected coincident constraint");
9687 };
9688 assert_eq!(
9689 coincident.segments,
9690 sketch
9691 .segments
9692 .iter()
9693 .copied()
9694 .map(Into::into)
9695 .collect::<Vec<ConstraintSegment>>()
9696 );
9697
9698 mock_ctx.close().await;
9699 }
9700
9701 #[tokio::test(flavor = "multi_thread")]
9702 async fn test_delete_line_preserves_multiline_equal_length_constraint() {
9703 let initial_source = "\
9704sketch(on = XY) {
9705 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9706 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9707 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9708 equalLength([line1, line2, line3])
9709}
9710";
9711
9712 let program = Program::parse(initial_source).unwrap().0.unwrap();
9713
9714 let mut frontend = FrontendState::new();
9715
9716 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9717 let mock_ctx = ExecutorContext::new_mock(None).await;
9718 let version = Version(0);
9719
9720 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9721 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9722 let sketch_id = sketch_object.id;
9723 let sketch = expect_sketch(sketch_object);
9724 let line3_id = *sketch.segments.get(8).unwrap();
9725
9726 let (src_delta, scene_delta) = frontend
9727 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
9728 .await
9729 .unwrap();
9730 assert_eq!(
9731 src_delta.text.as_str(),
9732 "\
9733sketch(on = XY) {
9734 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9735 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9736 equalLength([line1, line2])
9737}
9738"
9739 );
9740
9741 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9742 let sketch = expect_sketch(sketch_object);
9743 assert_eq!(sketch.constraints.len(), 1);
9744
9745 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9746 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9747 panic!("Expected constraint object");
9748 };
9749 let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
9750 panic!("Expected lines equal length constraint");
9751 };
9752 assert_eq!(lines_equal_length.lines.len(), 2);
9753
9754 ctx.close().await;
9755 mock_ctx.close().await;
9756 }
9757
9758 #[tokio::test(flavor = "multi_thread")]
9759 async fn test_delete_line_preserves_multiline_horizontal_constraint() {
9760 let initial_source = "\
9761sketch(on = XY) {
9762 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9763 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9764 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9765 horizontal([line1.end, line2.start, line3.start])
9766}
9767";
9768
9769 let program = Program::parse(initial_source).unwrap().0.unwrap();
9770
9771 let mut frontend = FrontendState::new();
9772
9773 let mock_ctx = ExecutorContext::new_mock(None).await;
9774 let version = Version(0);
9775
9776 frontend.program = program.clone();
9777 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9778 frontend.update_state_after_exec(outcome, true);
9779 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9780 let sketch_id = sketch_object.id;
9781 let sketch = expect_sketch(sketch_object);
9782 let line1_id = *sketch.segments.get(2).unwrap();
9783
9784 let (src_delta, scene_delta) = frontend
9785 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9786 .await
9787 .unwrap();
9788 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9789 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9790 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9791 assert!(
9792 src_delta.text.contains("horizontal([line2.start, line3.start])"),
9793 "{}",
9794 src_delta.text
9795 );
9796
9797 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9798 let sketch = expect_sketch(sketch_object);
9799 assert_eq!(sketch.constraints.len(), 1);
9800
9801 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9802 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9803 panic!("Expected constraint object");
9804 };
9805 let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
9806 panic!("Expected horizontal points constraint");
9807 };
9808 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9809 assert_eq!(*points, remaining_points);
9810
9811 mock_ctx.close().await;
9812 }
9813
9814 #[tokio::test(flavor = "multi_thread")]
9815 async fn test_delete_line_preserves_multiline_vertical_constraint() {
9816 let initial_source = "\
9817sketch(on = XY) {
9818 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9819 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9820 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9821 vertical([line1.end, line2.start, line3.start])
9822}
9823";
9824
9825 let program = Program::parse(initial_source).unwrap().0.unwrap();
9826
9827 let mut frontend = FrontendState::new();
9828
9829 let mock_ctx = ExecutorContext::new_mock(None).await;
9830 let version = Version(0);
9831
9832 frontend.program = program.clone();
9833 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9834 frontend.update_state_after_exec(outcome, true);
9835 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9836 let sketch_id = sketch_object.id;
9837 let sketch = expect_sketch(sketch_object);
9838 let line1_id = *sketch.segments.get(2).unwrap();
9839
9840 let (src_delta, scene_delta) = frontend
9841 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9842 .await
9843 .unwrap();
9844 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9845 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9846 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9847 assert!(
9848 src_delta.text.contains("vertical([line2.start, line3.start])"),
9849 "{}",
9850 src_delta.text
9851 );
9852
9853 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9854 let sketch = expect_sketch(sketch_object);
9855 assert_eq!(sketch.constraints.len(), 1);
9856
9857 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9858 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9859 panic!("Expected constraint object");
9860 };
9861 let Constraint::Vertical(Vertical::Points { points }) = constraint else {
9862 panic!("Expected vertical points constraint");
9863 };
9864 let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9865 assert_eq!(*points, remaining_points);
9866
9867 mock_ctx.close().await;
9868 }
9869
9870 #[tokio::test(flavor = "multi_thread")]
9871 async fn test_delete_line_preserves_multiline_coincident_constraint() {
9872 let initial_source = "\
9873sketch(on = XY) {
9874 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9875 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9876 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9877 coincident([line1.end, line2.start, line3.start])
9878}
9879";
9880
9881 let program = Program::parse(initial_source).unwrap().0.unwrap();
9882
9883 let mut frontend = FrontendState::new();
9884
9885 let mock_ctx = ExecutorContext::new_mock(None).await;
9886 let version = Version(0);
9887
9888 frontend.program = program.clone();
9889 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9890 frontend.update_state_after_exec(outcome, true);
9891 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9892 let sketch_id = sketch_object.id;
9893 let sketch = expect_sketch(sketch_object);
9894 let line1_id = *sketch.segments.get(2).unwrap();
9895
9896 let (src_delta, scene_delta) = frontend
9897 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9898 .await
9899 .unwrap();
9900 assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
9901 assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
9902 assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
9903 assert!(
9904 src_delta.text.contains("coincident([line2.start, line3.start])"),
9905 "{}",
9906 src_delta.text
9907 );
9908
9909 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9910 let sketch = expect_sketch(sketch_object);
9911 assert_eq!(sketch.constraints.len(), 1);
9912
9913 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
9914 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
9915 panic!("Expected constraint object");
9916 };
9917 let Constraint::Coincident(coincident) = constraint else {
9918 panic!("Expected coincident constraint");
9919 };
9920 let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
9921 assert_eq!(coincident.segments, remaining_segments);
9922
9923 mock_ctx.close().await;
9924 }
9925
9926 #[tokio::test(flavor = "multi_thread")]
9927 async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
9928 let initial_source = "\
9929sketch(on = XY) {
9930 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9931 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9932 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9933 equalLength([line1, line2, line3])
9934}
9935";
9936
9937 let program = Program::parse(initial_source).unwrap().0.unwrap();
9938
9939 let mut frontend = FrontendState::new();
9940
9941 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9942 let mock_ctx = ExecutorContext::new_mock(None).await;
9943 let version = Version(0);
9944
9945 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9946 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9947 let sketch_id = sketch_object.id;
9948 let sketch = expect_sketch(sketch_object);
9949 let line2_id = *sketch.segments.get(5).unwrap();
9950 let line3_id = *sketch.segments.get(8).unwrap();
9951
9952 let (src_delta, scene_delta) = frontend
9953 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
9954 .await
9955 .unwrap();
9956 assert_eq!(
9957 src_delta.text.as_str(),
9958 "\
9959sketch(on = XY) {
9960 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9961}
9962"
9963 );
9964
9965 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
9966 let sketch = expect_sketch(sketch_object);
9967 assert!(sketch.constraints.is_empty());
9968
9969 ctx.close().await;
9970 mock_ctx.close().await;
9971 }
9972
9973 #[tokio::test(flavor = "multi_thread")]
9974 async fn test_delete_line_preserves_multiline_parallel_constraint() {
9975 let initial_source = "\
9976sketch(on = XY) {
9977 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9978 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9979 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
9980 parallel([line1, line2, line3])
9981}
9982";
9983
9984 let program = Program::parse(initial_source).unwrap().0.unwrap();
9985
9986 let mut frontend = FrontendState::new();
9987
9988 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
9989 let mock_ctx = ExecutorContext::new_mock(None).await;
9990 let version = Version(0);
9991
9992 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9993 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9994 let sketch_id = sketch_object.id;
9995 let sketch = expect_sketch(sketch_object);
9996 let line3_id = *sketch.segments.get(8).unwrap();
9997
9998 let (src_delta, scene_delta) = frontend
9999 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10000 .await
10001 .unwrap();
10002 assert_eq!(
10003 src_delta.text.as_str(),
10004 "\
10005sketch(on = XY) {
10006 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10007 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10008 parallel([line1, line2])
10009}
10010"
10011 );
10012
10013 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10014 let sketch = expect_sketch(sketch_object);
10015 assert_eq!(sketch.constraints.len(), 1);
10016
10017 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10018 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10019 panic!("Expected constraint object");
10020 };
10021 let Constraint::Parallel(parallel) = constraint else {
10022 panic!("Expected parallel constraint");
10023 };
10024 assert_eq!(parallel.lines.len(), 2);
10025
10026 ctx.close().await;
10027 mock_ctx.close().await;
10028 }
10029
10030 #[tokio::test(flavor = "multi_thread")]
10031 async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10032 let initial_source = "\
10033sketch(on = XY) {
10034 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10035 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10036 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10037 parallel([line1, line2, line3])
10038}
10039";
10040
10041 let program = Program::parse(initial_source).unwrap().0.unwrap();
10042
10043 let mut frontend = FrontendState::new();
10044
10045 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10046 let mock_ctx = ExecutorContext::new_mock(None).await;
10047 let version = Version(0);
10048
10049 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10050 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10051 let sketch_id = sketch_object.id;
10052 let sketch = expect_sketch(sketch_object);
10053 let line2_id = *sketch.segments.get(5).unwrap();
10054 let line3_id = *sketch.segments.get(8).unwrap();
10055
10056 let (src_delta, scene_delta) = frontend
10057 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10058 .await
10059 .unwrap();
10060 assert_eq!(
10061 src_delta.text.as_str(),
10062 "\
10063sketch(on = XY) {
10064 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10065}
10066"
10067 );
10068
10069 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10070 let sketch = expect_sketch(sketch_object);
10071 assert!(sketch.constraints.is_empty());
10072
10073 ctx.close().await;
10074 mock_ctx.close().await;
10075 }
10076
10077 #[tokio::test(flavor = "multi_thread")]
10078 async fn test_delete_line_line_coincident_constraint() {
10079 let initial_source = "\
10080sketch(on = XY) {
10081 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10082 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10083 coincident([line1, line2])
10084}
10085";
10086
10087 let program = Program::parse(initial_source).unwrap().0.unwrap();
10088
10089 let mut frontend = FrontendState::new();
10090
10091 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10092 let mock_ctx = ExecutorContext::new_mock(None).await;
10093 let version = Version(0);
10094
10095 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10096 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10097 let sketch_id = sketch_object.id;
10098 let sketch = expect_sketch(sketch_object);
10099
10100 let coincident_id = *sketch.constraints.first().unwrap();
10101
10102 let (src_delta, scene_delta) = frontend
10103 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10104 .await
10105 .unwrap();
10106 assert_eq!(
10107 src_delta.text.as_str(),
10108 "\
10109sketch(on = XY) {
10110 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10111 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10112}
10113"
10114 );
10115 assert_eq!(scene_delta.new_objects, vec![]);
10116 assert_eq!(scene_delta.new_graph.objects.len(), 8);
10117
10118 ctx.close().await;
10119 mock_ctx.close().await;
10120 }
10121
10122 #[tokio::test(flavor = "multi_thread")]
10123 async fn test_two_points_coincident() {
10124 let initial_source = "\
10125sketch(on = XY) {
10126 point1 = point(at = [var 1, var 2])
10127 point(at = [3, 4])
10128}
10129";
10130
10131 let program = Program::parse(initial_source).unwrap().0.unwrap();
10132
10133 let mut frontend = FrontendState::new();
10134
10135 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10136 let mock_ctx = ExecutorContext::new_mock(None).await;
10137 let version = Version(0);
10138
10139 frontend.hack_set_program(&ctx, program).await.unwrap();
10140 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10141 let sketch_id = sketch_object.id;
10142 let sketch = expect_sketch(sketch_object);
10143 let point0_id = *sketch.segments.first().unwrap();
10144 let point1_id = *sketch.segments.get(1).unwrap();
10145
10146 let constraint = Constraint::Coincident(Coincident {
10147 segments: vec![point0_id.into(), point1_id.into()],
10148 });
10149 let (src_delta, scene_delta) = frontend
10150 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10151 .await
10152 .unwrap();
10153 assert_eq!(
10154 src_delta.text.as_str(),
10155 "\
10156sketch(on = XY) {
10157 point1 = point(at = [var 3, var 4])
10158 point2 = point(at = [3, 4])
10159 coincident([point1, point2])
10160}
10161"
10162 );
10163 assert_eq!(
10164 scene_delta.new_graph.objects.len(),
10165 5,
10166 "{:#?}",
10167 scene_delta.new_graph.objects
10168 );
10169
10170 ctx.close().await;
10171 mock_ctx.close().await;
10172 }
10173
10174 #[tokio::test(flavor = "multi_thread")]
10175 async fn test_three_points_coincident() {
10176 let initial_source = "\
10177sketch(on = XY) {
10178 point1 = point(at = [var 1, var 2])
10179 point(at = [var 3, var 4])
10180 point(at = [var 5, var 6])
10181}
10182";
10183
10184 let program = Program::parse(initial_source).unwrap().0.unwrap();
10185
10186 let mut frontend = FrontendState::new();
10187
10188 let mock_ctx = ExecutorContext::new_mock(None).await;
10189 let version = Version(0);
10190
10191 frontend.program = program.clone();
10192 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10193 frontend.update_state_after_exec(outcome, true);
10194 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10195 let sketch_id = sketch_object.id;
10196 let sketch = expect_sketch(sketch_object);
10197 let segments = sketch
10198 .segments
10199 .iter()
10200 .take(3)
10201 .copied()
10202 .map(Into::into)
10203 .collect::<Vec<ConstraintSegment>>();
10204
10205 let constraint = Constraint::Coincident(Coincident {
10206 segments: segments.clone(),
10207 });
10208 let (src_delta, scene_delta) = frontend
10209 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10210 .await
10211 .unwrap();
10212 assert_eq!(
10213 src_delta.text.as_str(),
10214 "\
10215sketch(on = XY) {
10216 point1 = point(at = [var 3, var 4])
10217 point2 = point(at = [var 3, var 4])
10218 point3 = point(at = [var 3, var 4])
10219 coincident([point1, point2, point3])
10220}
10221"
10222 );
10223
10224 let constraint_object = scene_delta
10225 .new_graph
10226 .objects
10227 .iter()
10228 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10229 .unwrap();
10230
10231 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10232 panic!("expected a constraint object");
10233 };
10234
10235 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10236
10237 mock_ctx.close().await;
10238 }
10239
10240 #[tokio::test(flavor = "multi_thread")]
10241 async fn test_source_with_three_point_coincident_tracks_all_segments() {
10242 let initial_source = "\
10243sketch(on = XY) {
10244 point1 = point(at = [var 1, var 2])
10245 point2 = point(at = [var 3, var 4])
10246 point3 = point(at = [var 5, var 6])
10247 coincident([point1, point2, point3])
10248}
10249";
10250
10251 let program = Program::parse(initial_source).unwrap().0.unwrap();
10252
10253 let mut frontend = FrontendState::new();
10254
10255 let ctx = ExecutorContext::new_mock(None).await;
10256 frontend.program = program.clone();
10257 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10258 frontend.update_state_after_exec(outcome, true);
10259
10260 let constraint_object = frontend
10261 .scene_graph
10262 .objects
10263 .iter()
10264 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10265 .unwrap();
10266 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10267 panic!("expected a constraint object");
10268 };
10269
10270 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10271 let sketch = expect_sketch(sketch_object);
10272 let expected_segments = sketch
10273 .segments
10274 .iter()
10275 .take(3)
10276 .copied()
10277 .map(Into::into)
10278 .collect::<Vec<ConstraintSegment>>();
10279
10280 assert_eq!(
10281 constraint,
10282 &Constraint::Coincident(Coincident {
10283 segments: expected_segments,
10284 })
10285 );
10286
10287 ctx.close().await;
10288 }
10289
10290 #[tokio::test(flavor = "multi_thread")]
10291 async fn test_point_origin_coincident_preserves_order() {
10292 let initial_source = "\
10293sketch(on = XY) {
10294 point(at = [var 1, var 2])
10295}
10296";
10297
10298 for (origin_first, expected_source) in [
10299 (
10300 true,
10301 "\
10302sketch(on = XY) {
10303 point1 = point(at = [var 0, var 0])
10304 coincident([ORIGIN, point1])
10305}
10306",
10307 ),
10308 (
10309 false,
10310 "\
10311sketch(on = XY) {
10312 point1 = point(at = [var 0, var 0])
10313 coincident([point1, ORIGIN])
10314}
10315",
10316 ),
10317 ] {
10318 let program = Program::parse(initial_source).unwrap().0.unwrap();
10319
10320 let mut frontend = FrontendState::new();
10321
10322 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10323 let mock_ctx = ExecutorContext::new_mock(None).await;
10324 let version = Version(0);
10325
10326 frontend.hack_set_program(&ctx, program).await.unwrap();
10327 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10328 let sketch_id = sketch_object.id;
10329 let sketch = expect_sketch(sketch_object);
10330 let point_id = *sketch.segments.first().unwrap();
10331
10332 let segments = if origin_first {
10333 vec![ConstraintSegment::ORIGIN, point_id.into()]
10334 } else {
10335 vec![point_id.into(), ConstraintSegment::ORIGIN]
10336 };
10337 let constraint = Constraint::Coincident(Coincident {
10338 segments: segments.clone(),
10339 });
10340 let (src_delta, scene_delta) = frontend
10341 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10342 .await
10343 .unwrap();
10344 assert_eq!(src_delta.text.as_str(), expected_source);
10345
10346 let constraint_object = scene_delta
10347 .new_graph
10348 .objects
10349 .iter()
10350 .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10351 .unwrap();
10352
10353 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10354 panic!("expected a constraint object");
10355 };
10356
10357 assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10358
10359 ctx.close().await;
10360 mock_ctx.close().await;
10361 }
10362 }
10363
10364 #[tokio::test(flavor = "multi_thread")]
10365 async fn test_coincident_of_line_end_points() {
10366 let initial_source = "\
10367sketch(on = XY) {
10368 line(start = [var 1, var 2], end = [var 3, var 4])
10369 line(start = [var 5, var 6], end = [var 7, var 8])
10370}
10371";
10372
10373 let program = Program::parse(initial_source).unwrap().0.unwrap();
10374
10375 let mut frontend = FrontendState::new();
10376
10377 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10378 let mock_ctx = ExecutorContext::new_mock(None).await;
10379 let version = Version(0);
10380
10381 frontend.hack_set_program(&ctx, program).await.unwrap();
10382 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10383 let sketch_id = sketch_object.id;
10384 let sketch = expect_sketch(sketch_object);
10385 let point0_id = *sketch.segments.get(1).unwrap();
10386 let point1_id = *sketch.segments.get(3).unwrap();
10387
10388 let constraint = Constraint::Coincident(Coincident {
10389 segments: vec![point0_id.into(), point1_id.into()],
10390 });
10391 let (src_delta, scene_delta) = frontend
10392 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10393 .await
10394 .unwrap();
10395 assert_eq!(
10396 src_delta.text.as_str(),
10397 "\
10398sketch(on = XY) {
10399 line1 = line(start = [var 1, var 2], end = [var 4, var 5])
10400 line2 = line(start = [var 4, var 5], end = [var 7, var 8])
10401 coincident([line1.end, line2.start])
10402}
10403"
10404 );
10405 assert_eq!(
10406 scene_delta.new_graph.objects.len(),
10407 9,
10408 "{:#?}",
10409 scene_delta.new_graph.objects
10410 );
10411
10412 ctx.close().await;
10413 mock_ctx.close().await;
10414 }
10415
10416 #[tokio::test(flavor = "multi_thread")]
10417 async fn test_coincident_of_line_point_and_circle_segment() {
10418 let initial_source = "\
10419sketch(on = XY) {
10420 circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10421 line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10422}
10423";
10424 let program = Program::parse(initial_source).unwrap().0.unwrap();
10425 let mut frontend = FrontendState::new();
10426
10427 let mock_ctx = ExecutorContext::new_mock(None).await;
10428 let version = Version(0);
10429
10430 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10431 frontend.program = program;
10432 frontend.update_state_after_exec(outcome, true);
10433 let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10434 let sketch_id = sketch_object.id;
10435 let sketch = expect_sketch(sketch_object);
10436
10437 let circle_id = sketch
10438 .segments
10439 .iter()
10440 .copied()
10441 .find(|seg_id| {
10442 matches!(
10443 &frontend.scene_graph.objects[seg_id.0].kind,
10444 ObjectKind::Segment {
10445 segment: Segment::Circle(_)
10446 }
10447 )
10448 })
10449 .expect("Expected a circle segment in sketch");
10450 let line_id = frontend
10451 .scene_graph
10452 .objects
10453 .iter()
10454 .find_map(|obj| match &obj.kind {
10455 ObjectKind::Segment {
10456 segment: Segment::Line(line),
10457 } if line.owner.is_none() => Some(obj.id),
10458 _ => None,
10459 })
10460 .expect("Expected a standalone line segment in scene graph");
10461
10462 let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10463 ObjectKind::Segment {
10464 segment: Segment::Line(line),
10465 } => line.start,
10466 _ => panic!("Expected line segment object"),
10467 };
10468
10469 let constraint = Constraint::Coincident(Coincident {
10470 segments: vec![line_start_point_id.into(), circle_id.into()],
10471 });
10472 let (src_delta, _scene_delta) = frontend
10473 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10474 .await
10475 .unwrap();
10476 assert_eq!(
10477 src_delta.text.as_str(),
10478 "\
10479sketch(on = XY) {
10480 circle1 = circle(start = [var 7.02mm, var 0mm], center = [var -0.01mm, var 0.22mm])
10481 line1 = line(start = [var 7mm, var 0.78mm], end = [var 10mm, var 2mm])
10482 coincident([line1.start, circle1])
10483}
10484"
10485 );
10486
10487 mock_ctx.close().await;
10488 }
10489
10490 #[tokio::test(flavor = "multi_thread")]
10491 async fn test_invalid_coincident_arc_and_line_preserves_state() {
10492 let program = Program::empty();
10500
10501 let mut frontend = FrontendState::new();
10502 frontend.program = program;
10503
10504 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10505 let mock_ctx = ExecutorContext::new_mock(None).await;
10506 let version = Version(0);
10507
10508 let sketch_args = SketchCtor {
10509 on: Plane::Default(PlaneName::Xy),
10510 };
10511 let (_src_delta, _scene_delta, sketch_id) = frontend
10512 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10513 .await
10514 .unwrap();
10515
10516 let arc_ctor = ArcCtor {
10518 start: Point2d {
10519 x: Expr::Var(Number {
10520 value: 0.0,
10521 units: NumericSuffix::Mm,
10522 }),
10523 y: Expr::Var(Number {
10524 value: 0.0,
10525 units: NumericSuffix::Mm,
10526 }),
10527 },
10528 end: Point2d {
10529 x: Expr::Var(Number {
10530 value: 10.0,
10531 units: NumericSuffix::Mm,
10532 }),
10533 y: Expr::Var(Number {
10534 value: 10.0,
10535 units: NumericSuffix::Mm,
10536 }),
10537 },
10538 center: Point2d {
10539 x: Expr::Var(Number {
10540 value: 10.0,
10541 units: NumericSuffix::Mm,
10542 }),
10543 y: Expr::Var(Number {
10544 value: 0.0,
10545 units: NumericSuffix::Mm,
10546 }),
10547 },
10548 construction: None,
10549 };
10550 let (_src_delta, scene_delta) = frontend
10551 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10552 .await
10553 .unwrap();
10554 let arc_id = *scene_delta.new_objects.last().unwrap();
10556
10557 let line_ctor = LineCtor {
10559 start: Point2d {
10560 x: Expr::Var(Number {
10561 value: 20.0,
10562 units: NumericSuffix::Mm,
10563 }),
10564 y: Expr::Var(Number {
10565 value: 0.0,
10566 units: NumericSuffix::Mm,
10567 }),
10568 },
10569 end: Point2d {
10570 x: Expr::Var(Number {
10571 value: 30.0,
10572 units: NumericSuffix::Mm,
10573 }),
10574 y: Expr::Var(Number {
10575 value: 10.0,
10576 units: NumericSuffix::Mm,
10577 }),
10578 },
10579 construction: None,
10580 };
10581 let (_src_delta, scene_delta) = frontend
10582 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10583 .await
10584 .unwrap();
10585 let line_id = *scene_delta.new_objects.last().unwrap();
10587
10588 let constraint = Constraint::Coincident(Coincident {
10591 segments: vec![arc_id.into(), line_id.into()],
10592 });
10593 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10594
10595 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10597
10598 let sketch_object_after =
10601 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10602 let sketch_after = expect_sketch(sketch_object_after);
10603
10604 assert!(
10606 sketch_after.segments.contains(&arc_id),
10607 "Arc segment should still exist after failed constraint"
10608 );
10609 assert!(
10610 sketch_after.segments.contains(&line_id),
10611 "Line segment should still exist after failed constraint"
10612 );
10613
10614 let arc_obj = frontend
10616 .scene_graph
10617 .objects
10618 .get(arc_id.0)
10619 .expect("Arc object should still be accessible");
10620 let line_obj = frontend
10621 .scene_graph
10622 .objects
10623 .get(line_id.0)
10624 .expect("Line object should still be accessible");
10625
10626 match &arc_obj.kind {
10629 ObjectKind::Segment {
10630 segment: Segment::Arc(_),
10631 } => {}
10632 _ => panic!("Arc object should still be an arc segment"),
10633 }
10634 match &line_obj.kind {
10635 ObjectKind::Segment {
10636 segment: Segment::Line(_),
10637 } => {}
10638 _ => panic!("Line object should still be a line segment"),
10639 }
10640
10641 ctx.close().await;
10642 mock_ctx.close().await;
10643 }
10644
10645 #[tokio::test(flavor = "multi_thread")]
10646 async fn test_distance_two_points() {
10647 let initial_source = "\
10648sketch(on = XY) {
10649 point(at = [var 1, var 2])
10650 point(at = [var 3, var 4])
10651}
10652";
10653
10654 let program = Program::parse(initial_source).unwrap().0.unwrap();
10655
10656 let mut frontend = FrontendState::new();
10657
10658 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10659 let mock_ctx = ExecutorContext::new_mock(None).await;
10660 let version = Version(0);
10661
10662 frontend.hack_set_program(&ctx, program).await.unwrap();
10663 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10664 let sketch_id = sketch_object.id;
10665 let sketch = expect_sketch(sketch_object);
10666 let point0_id = *sketch.segments.first().unwrap();
10667 let point1_id = *sketch.segments.get(1).unwrap();
10668
10669 let constraint = Constraint::Distance(Distance {
10670 points: vec![point0_id.into(), point1_id.into()],
10671 distance: Number {
10672 value: 2.0,
10673 units: NumericSuffix::Mm,
10674 },
10675 label_position: None,
10676 source: Default::default(),
10677 });
10678 let (src_delta, scene_delta) = frontend
10679 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10680 .await
10681 .unwrap();
10682 assert_eq!(
10683 src_delta.text.as_str(),
10684 "\
10686sketch(on = XY) {
10687 point1 = point(at = [var 1.29, var 2.29])
10688 point2 = point(at = [var 2.71, var 3.71])
10689 distance([point1, point2]) == 2mm
10690}
10691"
10692 );
10693 assert_eq!(
10694 scene_delta.new_graph.objects.len(),
10695 5,
10696 "{:#?}",
10697 scene_delta.new_graph.objects
10698 );
10699
10700 ctx.close().await;
10701 mock_ctx.close().await;
10702 }
10703
10704 #[tokio::test(flavor = "multi_thread")]
10705 async fn test_distance_two_points_with_label() {
10706 let initial_source = "\
10707sketch(on = XY) {
10708 point(at = [var 1, var 2])
10709 point(at = [var 3, var 4])
10710}
10711";
10712
10713 let program = Program::parse(initial_source).unwrap().0.unwrap();
10714
10715 let mut frontend = FrontendState::new();
10716
10717 let mock_ctx = ExecutorContext::new_mock(None).await;
10718 let version = Version(0);
10719
10720 frontend.program = program.clone();
10721 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10722 frontend.update_state_after_exec(outcome, true);
10723 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10724 let sketch_id = sketch_object.id;
10725 let sketch = expect_sketch(sketch_object);
10726 let point0_id = *sketch.segments.first().unwrap();
10727 let point1_id = *sketch.segments.get(1).unwrap();
10728
10729 let label_position = Point2d {
10730 x: Number {
10731 value: 10.0,
10732 units: NumericSuffix::Mm,
10733 },
10734 y: Number {
10735 value: 11.0,
10736 units: NumericSuffix::Mm,
10737 },
10738 };
10739 let constraint = Constraint::Distance(Distance {
10740 points: vec![point0_id.into(), point1_id.into()],
10741 distance: Number {
10742 value: 2.0,
10743 units: NumericSuffix::Mm,
10744 },
10745 label_position: Some(label_position.clone()),
10746 source: Default::default(),
10747 });
10748 let (src_delta, scene_delta) = frontend
10749 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10750 .await
10751 .unwrap();
10752 assert_eq!(
10753 src_delta.text.as_str(),
10754 "\
10755sketch(on = XY) {
10756 point1 = point(at = [var 1.29, var 2.29])
10757 point2 = point(at = [var 2.71, var 3.71])
10758 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10759}
10760"
10761 );
10762
10763 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10764 let sketch = expect_sketch(sketch_object);
10765 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10766 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10767 panic!("Expected constraint object");
10768 };
10769 let Constraint::Distance(distance) = constraint else {
10770 panic!("Expected distance constraint");
10771 };
10772 assert_eq!(distance.label_position, Some(label_position));
10773
10774 mock_ctx.close().await;
10775 }
10776
10777 #[tokio::test(flavor = "multi_thread")]
10778 async fn test_edit_distance_constraint_label_position() {
10779 let initial_source = "\
10780sketch(on = XY) {
10781 point(at = [var 1, var 2])
10782 point(at = [var 3, var 2])
10783}
10784";
10785
10786 let program = Program::parse(initial_source).unwrap().0.unwrap();
10787
10788 let mut frontend = FrontendState::new();
10789
10790 let mock_ctx = ExecutorContext::new_mock(None).await;
10791 let version = Version(0);
10792
10793 frontend.program = program.clone();
10794 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10795 frontend.update_state_after_exec(outcome, true);
10796 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10797 let sketch_id = sketch_object.id;
10798 let sketch = expect_sketch(sketch_object);
10799 let point0_id = *sketch.segments.first().unwrap();
10800 let point1_id = *sketch.segments.get(1).unwrap();
10801
10802 let constraint = Constraint::Distance(Distance {
10803 points: vec![point0_id.into(), point1_id.into()],
10804 distance: Number {
10805 value: 2.0,
10806 units: NumericSuffix::Mm,
10807 },
10808 label_position: None,
10809 source: Default::default(),
10810 });
10811 let (_, scene_delta) = frontend
10812 .add_constraint(&mock_ctx, version, sketch_id, constraint)
10813 .await
10814 .unwrap();
10815 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10816 let sketch = expect_sketch(sketch_object);
10817 let constraint_id = sketch.constraints[0];
10818 let label_position = Point2d {
10819 x: Number {
10820 value: 10.0,
10821 units: NumericSuffix::Mm,
10822 },
10823 y: Number {
10824 value: 11.0,
10825 units: NumericSuffix::Mm,
10826 },
10827 };
10828
10829 let (src_delta, scene_delta) = frontend
10830 .edit_distance_constraint_label_position(
10831 &mock_ctx,
10832 version,
10833 sketch_id,
10834 constraint_id,
10835 label_position.clone(),
10836 vec![],
10837 )
10838 .await
10839 .unwrap();
10840 assert_eq!(
10841 src_delta.text.as_str(),
10842 "\
10843sketch(on = XY) {
10844 point1 = point(at = [var 1, var 2])
10845 point2 = point(at = [var 3, var 2])
10846 distance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
10847}
10848"
10849 );
10850
10851 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
10852 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10853 panic!("Expected constraint object");
10854 };
10855 let Constraint::Distance(distance) = constraint else {
10856 panic!("Expected distance constraint");
10857 };
10858 assert_eq!(distance.label_position, Some(label_position));
10859
10860 mock_ctx.close().await;
10861 }
10862
10863 #[tokio::test(flavor = "multi_thread")]
10864 async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
10865 let initial_source = "\
10866sketch(on = XY) {
10867 point1 = point(at = [var 0mm, var 0mm])
10868 point2 = point(at = [var 10mm, var 0mm])
10869 distance([point1, point2]) == 5mm
10870}
10871";
10872
10873 let program = Program::parse(initial_source).unwrap().0.unwrap();
10874 let mut frontend = FrontendState::new();
10875 let mock_ctx = ExecutorContext::new_mock(None).await;
10876 let version = Version(0);
10877
10878 frontend.program = program.clone();
10879 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10880 frontend.update_state_after_exec(outcome, true);
10881 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10882 let sketch_id = sketch_object.id;
10883 let sketch = expect_sketch(sketch_object);
10884 let point0_id = sketch.segments[0];
10885 let point1_id = sketch.segments[1];
10886 let constraint_id = sketch.constraints[0];
10887
10888 let edited_segments = vec![ExistingSegmentCtor {
10889 id: point0_id,
10890 ctor: SegmentCtor::Point(PointCtor {
10891 position: Point2d {
10892 x: Expr::Var(Number {
10893 value: 2.0,
10894 units: NumericSuffix::Mm,
10895 }),
10896 y: Expr::Var(Number {
10897 value: 1.0,
10898 units: NumericSuffix::Mm,
10899 }),
10900 },
10901 }),
10902 }];
10903 let (_, scene_delta) = frontend
10904 .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
10905 .await
10906 .unwrap();
10907 let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
10908 let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
10909
10910 let label_position = Point2d {
10911 x: Number {
10912 value: 3.0,
10913 units: NumericSuffix::Mm,
10914 },
10915 y: Number {
10916 value: 4.0,
10917 units: NumericSuffix::Mm,
10918 },
10919 };
10920 let (_, scene_delta) = frontend
10921 .edit_distance_constraint_label_position(
10922 &mock_ctx,
10923 version,
10924 sketch_id,
10925 constraint_id,
10926 label_position,
10927 vec![point0_id],
10928 )
10929 .await
10930 .unwrap();
10931
10932 assert_point_position_close(
10933 point_position(&scene_delta.new_graph, point0_id),
10934 point0_after_segment_edit,
10935 );
10936 assert_point_position_close(
10937 point_position(&scene_delta.new_graph, point1_id),
10938 point1_after_segment_edit,
10939 );
10940
10941 mock_ctx.close().await;
10942 }
10943
10944 #[tokio::test(flavor = "multi_thread")]
10945 async fn test_distance_point_line() {
10946 let initial_source = "\
10947sketch(on = XY) {
10948 point(at = [var 0, var 5])
10949 line(start = [var 0, var 0], end = [var 10, var 0])
10950}
10951";
10952
10953 let program = Program::parse(initial_source).unwrap().0.unwrap();
10954
10955 let mut frontend = FrontendState::new();
10956
10957 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
10958 let mock_ctx = ExecutorContext::new_mock(None).await;
10959 let version = Version(0);
10960
10961 frontend.hack_set_program(&ctx, program).await.unwrap();
10962 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10963 let sketch_id = sketch_object.id;
10964 let sketch = expect_sketch(sketch_object);
10965 let point_id = *sketch.segments.first().unwrap();
10966 let line_id = *sketch
10967 .segments
10968 .iter()
10969 .find(|segment_id| {
10970 matches!(
10971 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
10972 Some(ObjectKind::Segment {
10973 segment: Segment::Line(_)
10974 })
10975 )
10976 })
10977 .unwrap();
10978
10979 let label_position = Point2d {
10980 x: Number {
10981 value: 10.0,
10982 units: NumericSuffix::Mm,
10983 },
10984 y: Number {
10985 value: 11.0,
10986 units: NumericSuffix::Mm,
10987 },
10988 };
10989 let constraint = Constraint::Distance(Distance {
10990 points: vec![point_id.into(), line_id.into()],
10991 distance: Number {
10992 value: 5.0,
10993 units: NumericSuffix::Mm,
10994 },
10995 label_position: Some(label_position.clone()),
10996 source: Default::default(),
10997 });
10998 let (src_delta, scene_delta) = frontend
10999 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11000 .await
11001 .unwrap();
11002 assert_eq!(
11003 src_delta.text.as_str(),
11004 "\
11005sketch(on = XY) {
11006 point1 = point(at = [var 0, var 5])
11007 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
11008 distance([point1, line1], labelPosition = [10mm, 11mm]) == 5mm
11009}
11010"
11011 );
11012 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11013 let sketch = expect_sketch(sketch_object);
11014 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11015 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11016 panic!("Expected constraint object");
11017 };
11018 let Constraint::Distance(distance) = constraint else {
11019 panic!("Expected distance constraint");
11020 };
11021 assert_eq!(distance.label_position, Some(label_position));
11022
11023 ctx.close().await;
11024 mock_ctx.close().await;
11025 }
11026
11027 #[tokio::test(flavor = "multi_thread")]
11028 async fn test_distance_point_arc() {
11029 let initial_source = "\
11030sketch(on = XY) {
11031 point(at = [var 0, var 8])
11032 arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11033}
11034";
11035
11036 let program = Program::parse(initial_source).unwrap().0.unwrap();
11037
11038 let mut frontend = FrontendState::new();
11039
11040 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11041 let mock_ctx = ExecutorContext::new_mock(None).await;
11042 let version = Version(0);
11043
11044 frontend.hack_set_program(&ctx, program).await.unwrap();
11045 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11046 let sketch_id = sketch_object.id;
11047 let sketch = expect_sketch(sketch_object);
11048 let point_id = *sketch.segments.first().unwrap();
11049 let arc_id = *sketch
11050 .segments
11051 .iter()
11052 .find(|segment_id| {
11053 matches!(
11054 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11055 Some(ObjectKind::Segment {
11056 segment: Segment::Arc(_)
11057 })
11058 )
11059 })
11060 .unwrap();
11061
11062 let constraint = Constraint::Distance(Distance {
11063 points: vec![point_id.into(), arc_id.into()],
11064 distance: Number {
11065 value: 3.0,
11066 units: NumericSuffix::Mm,
11067 },
11068 label_position: None,
11069 source: Default::default(),
11070 });
11071 let (src_delta, _scene_delta) = frontend
11072 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11073 .await
11074 .unwrap();
11075 assert_eq!(
11076 src_delta.text.as_str(),
11077 "\
11078sketch(on = XY) {
11079 point1 = point(at = [var 0, var 8])
11080 arc1 = arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11081 distance([point1, arc1]) == 3mm
11082}
11083"
11084 );
11085
11086 ctx.close().await;
11087 mock_ctx.close().await;
11088 }
11089
11090 #[tokio::test(flavor = "multi_thread")]
11091 async fn test_distance_arc_origin() {
11092 let initial_source = "\
11093sketch001 = sketch(on = XY) {
11094 arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11095}
11096";
11097
11098 let program = Program::parse(initial_source).unwrap().0.unwrap();
11099
11100 let mut frontend = FrontendState::new();
11101
11102 let mock_ctx = ExecutorContext::new_mock(None).await;
11103 let version = Version(0);
11104
11105 frontend.program = program.clone();
11106 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11107 frontend.update_state_after_exec(outcome, true);
11108 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11109 let sketch_id = sketch_object.id;
11110 let sketch = expect_sketch(sketch_object);
11111 let arc_id = *sketch
11112 .segments
11113 .iter()
11114 .find(|segment_id| {
11115 matches!(
11116 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11117 Some(ObjectKind::Segment {
11118 segment: Segment::Arc(_)
11119 })
11120 )
11121 })
11122 .unwrap();
11123
11124 let constraint = Constraint::Distance(Distance {
11125 points: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11126 distance: Number {
11127 value: 3.0,
11128 units: NumericSuffix::Mm,
11129 },
11130 label_position: None,
11131 source: Default::default(),
11132 });
11133 let (src_delta, _scene_delta) = frontend
11134 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11135 .await
11136 .unwrap();
11137 assert_eq!(
11138 src_delta.text.as_str(),
11139 "\
11140sketch001 = sketch(on = XY) {
11141 arc1 = arc(start = [var -4.16mm, var -0.43mm], end = [var -3.53mm, var 3.28mm], center = [var -4.91mm, var 1.61mm])
11142 distance([arc1, ORIGIN]) == 3mm
11143}
11144"
11145 );
11146
11147 mock_ctx.close().await;
11148 }
11149
11150 #[tokio::test(flavor = "multi_thread")]
11151 async fn test_distance_line_origin() {
11152 let initial_source = "\
11153sketch(on = XY) {
11154 line(start = [var 5, var 0], end = [var 5, var 10])
11155}
11156";
11157
11158 let program = Program::parse(initial_source).unwrap().0.unwrap();
11159
11160 let mut frontend = FrontendState::new();
11161
11162 let mock_ctx = ExecutorContext::new_mock(None).await;
11163 let version = Version(0);
11164
11165 frontend.program = program.clone();
11166 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11167 frontend.update_state_after_exec(outcome, true);
11168 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11169 let sketch_id = sketch_object.id;
11170 let sketch = expect_sketch(sketch_object);
11171 let line_id = *sketch
11172 .segments
11173 .iter()
11174 .find(|segment_id| {
11175 matches!(
11176 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11177 Some(ObjectKind::Segment {
11178 segment: Segment::Line(_)
11179 })
11180 )
11181 })
11182 .unwrap();
11183
11184 let constraint = Constraint::Distance(Distance {
11185 points: vec![ConstraintSegment::ORIGIN, line_id.into()],
11186 distance: Number {
11187 value: 5.0,
11188 units: NumericSuffix::Mm,
11189 },
11190 label_position: None,
11191 source: Default::default(),
11192 });
11193 let (src_delta, _scene_delta) = frontend
11194 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11195 .await
11196 .unwrap();
11197 assert_eq!(
11198 src_delta.text.as_str(),
11199 "\
11200sketch(on = XY) {
11201 line1 = line(start = [var 5, var 0], end = [var 5, var 10])
11202 distance([ORIGIN, line1]) == 5mm
11203}
11204"
11205 );
11206
11207 mock_ctx.close().await;
11208 }
11209
11210 #[tokio::test(flavor = "multi_thread")]
11211 async fn test_distance_line_circle() {
11212 let initial_source = "\
11213sketch(on = XY) {
11214 line(start = [var -10, var 8], end = [var 10, var 8])
11215 circle(start = [var 5, var 0], center = [var 0, var 0])
11216}
11217";
11218
11219 let program = Program::parse(initial_source).unwrap().0.unwrap();
11220
11221 let mut frontend = FrontendState::new();
11222
11223 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11224 let mock_ctx = ExecutorContext::new_mock(None).await;
11225 let version = Version(0);
11226
11227 frontend.hack_set_program(&ctx, program).await.unwrap();
11228 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11229 let sketch_id = sketch_object.id;
11230 let sketch = expect_sketch(sketch_object);
11231 let line_id = *sketch
11232 .segments
11233 .iter()
11234 .find(|segment_id| {
11235 matches!(
11236 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11237 Some(ObjectKind::Segment {
11238 segment: Segment::Line(_)
11239 })
11240 )
11241 })
11242 .unwrap();
11243 let circle_id = *sketch
11244 .segments
11245 .iter()
11246 .find(|segment_id| {
11247 matches!(
11248 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11249 Some(ObjectKind::Segment {
11250 segment: Segment::Circle(_)
11251 })
11252 )
11253 })
11254 .unwrap();
11255
11256 let constraint = Constraint::Distance(Distance {
11257 points: vec![line_id.into(), circle_id.into()],
11258 distance: Number {
11259 value: 3.0,
11260 units: NumericSuffix::Mm,
11261 },
11262 label_position: None,
11263 source: Default::default(),
11264 });
11265 let (src_delta, _scene_delta) = frontend
11266 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11267 .await
11268 .unwrap();
11269 assert_eq!(
11270 src_delta.text.as_str(),
11271 "\
11272sketch(on = XY) {
11273 line1 = line(start = [var -10, var 8], end = [var 10, var 8])
11274 circle1 = circle(start = [var 5, var 0], center = [var 0, var 0])
11275 distance([line1, circle1]) == 3mm
11276}
11277"
11278 );
11279
11280 ctx.close().await;
11281 mock_ctx.close().await;
11282 }
11283
11284 #[tokio::test(flavor = "multi_thread")]
11285 async fn test_distance_circle_arc() {
11286 let initial_source = "\
11287sketch(on = XY) {
11288 circle(start = [var 5, var 0], center = [var 0, var 0])
11289 arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
11290}
11291";
11292
11293 let program = Program::parse(initial_source).unwrap().0.unwrap();
11294
11295 let mut frontend = FrontendState::new();
11296
11297 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11298 let mock_ctx = ExecutorContext::new_mock(None).await;
11299 let version = Version(0);
11300
11301 frontend.hack_set_program(&ctx, program).await.unwrap();
11302 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11303 let sketch_id = sketch_object.id;
11304 let sketch = expect_sketch(sketch_object);
11305 let circle_id = *sketch
11306 .segments
11307 .iter()
11308 .find(|segment_id| {
11309 matches!(
11310 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11311 Some(ObjectKind::Segment {
11312 segment: Segment::Circle(_)
11313 })
11314 )
11315 })
11316 .unwrap();
11317 let arc_id = *sketch
11318 .segments
11319 .iter()
11320 .find(|segment_id| {
11321 matches!(
11322 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11323 Some(ObjectKind::Segment {
11324 segment: Segment::Arc(_)
11325 })
11326 )
11327 })
11328 .unwrap();
11329
11330 let constraint = Constraint::Distance(Distance {
11331 points: vec![circle_id.into(), arc_id.into()],
11332 distance: Number {
11333 value: 3.0,
11334 units: NumericSuffix::Mm,
11335 },
11336 label_position: None,
11337 source: Default::default(),
11338 });
11339 let (src_delta, _scene_delta) = frontend
11340 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11341 .await
11342 .unwrap();
11343 assert_eq!(
11344 src_delta.text.as_str(),
11345 "\
11346sketch(on = XY) {
11347 circle1 = circle(start = [var 4.33, var 0], center = [var -0.34, var -0.09])
11348 arc1 = arc(start = [var 15.33, var -0.01], end = [var 10.01, var 4.33], center = [var 11.34, var 0.53])
11349 distance([circle1, arc1]) == 3mm
11350}
11351"
11352 );
11353
11354 ctx.close().await;
11355 mock_ctx.close().await;
11356 }
11357
11358 #[tokio::test(flavor = "multi_thread")]
11359 async fn test_distance_parallel_lines() {
11360 let initial_source = "\
11361sketch(on = XY) {
11362 line(start = [var 0, var 0], end = [var 10, var 0])
11363 line(start = [var 0, var 5], end = [var 10, var 5])
11364}
11365";
11366
11367 let program = Program::parse(initial_source).unwrap().0.unwrap();
11368
11369 let mut frontend = FrontendState::new();
11370
11371 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11372 let mock_ctx = ExecutorContext::new_mock(None).await;
11373 let version = Version(0);
11374
11375 frontend.hack_set_program(&ctx, program).await.unwrap();
11376 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11377 let sketch_id = sketch_object.id;
11378 let sketch = expect_sketch(sketch_object);
11379 let line_ids = sketch
11380 .segments
11381 .iter()
11382 .copied()
11383 .filter(|segment_id| {
11384 matches!(
11385 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11386 Some(ObjectKind::Segment {
11387 segment: Segment::Line(_)
11388 })
11389 )
11390 })
11391 .collect::<Vec<_>>();
11392
11393 let constraint = Constraint::Distance(Distance {
11394 points: vec![line_ids[0].into(), line_ids[1].into()],
11395 distance: Number {
11396 value: 5.0,
11397 units: NumericSuffix::Mm,
11398 },
11399 label_position: None,
11400 source: Default::default(),
11401 });
11402 let (src_delta, _scene_delta) = frontend
11403 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11404 .await
11405 .unwrap();
11406 assert_eq!(
11407 src_delta.text.as_str(),
11408 "\
11409sketch(on = XY) {
11410 line1 = line(start = [var 0, var 0], end = [var 10, var 0])
11411 line2 = line(start = [var 0, var 5], end = [var 10, var 5])
11412 distance([line1, line2]) == 5mm
11413}
11414"
11415 );
11416
11417 ctx.close().await;
11418 mock_ctx.close().await;
11419 }
11420
11421 #[tokio::test(flavor = "multi_thread")]
11422 async fn test_distance_non_parallel_lines_lowers_to_distance() {
11423 let initial_source = "\
11424sketch(on = XY) {
11425 line(start = [var 0, var 0], end = [var 10, var 0])
11426 line(start = [var 0, var 0], end = [var 0, var 10])
11427}
11428";
11429
11430 let program = Program::parse(initial_source).unwrap().0.unwrap();
11431
11432 let mut frontend = FrontendState::new();
11433
11434 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11435 let mock_ctx = ExecutorContext::new_mock(None).await;
11436 let version = Version(0);
11437
11438 frontend.hack_set_program(&ctx, program).await.unwrap();
11439 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11440 let sketch_id = sketch_object.id;
11441 let sketch = expect_sketch(sketch_object);
11442 let line_ids = sketch
11443 .segments
11444 .iter()
11445 .copied()
11446 .filter(|segment_id| {
11447 matches!(
11448 frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11449 Some(ObjectKind::Segment {
11450 segment: Segment::Line(_)
11451 })
11452 )
11453 })
11454 .collect::<Vec<_>>();
11455
11456 let constraint = Constraint::Distance(Distance {
11457 points: vec![line_ids[0].into(), line_ids[1].into()],
11458 distance: Number {
11459 value: 5.0,
11460 units: NumericSuffix::Mm,
11461 },
11462 label_position: None,
11463 source: Default::default(),
11464 });
11465 let (src_delta, _scene_delta) = frontend
11466 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11467 .await
11468 .unwrap();
11469 assert_eq!(
11470 src_delta.text.as_str(),
11471 "\
11472sketch(on = XY) {
11473 line1 = line(start = [var 4.98, var -0.07], end = [var 4.98, var 0.14])
11474 line2 = line(start = [var 0.02, var 4.3], end = [var 0.03, var 5.65])
11475 distance([line1, line2]) == 5mm
11476}
11477"
11478 );
11479
11480 ctx.close().await;
11481 mock_ctx.close().await;
11482 }
11483
11484 #[tokio::test(flavor = "multi_thread")]
11485 async fn test_horizontal_distance_two_points() {
11486 let initial_source = "\
11487sketch(on = XY) {
11488 point(at = [var 1, var 2])
11489 point(at = [var 3, var 4])
11490}
11491";
11492
11493 let program = Program::parse(initial_source).unwrap().0.unwrap();
11494
11495 let mut frontend = FrontendState::new();
11496
11497 let mock_ctx = ExecutorContext::new_mock(None).await;
11498 let version = Version(0);
11499
11500 frontend.program = program.clone();
11501 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11502 frontend.update_state_after_exec(outcome, true);
11503 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11504 let sketch_id = sketch_object.id;
11505 let sketch = expect_sketch(sketch_object);
11506 let point0_id = *sketch.segments.first().unwrap();
11507 let point1_id = *sketch.segments.get(1).unwrap();
11508 let label_position = Point2d {
11509 x: Number {
11510 value: 10.0,
11511 units: NumericSuffix::Mm,
11512 },
11513 y: Number {
11514 value: 11.0,
11515 units: NumericSuffix::Mm,
11516 },
11517 };
11518
11519 let constraint = Constraint::HorizontalDistance(Distance {
11520 points: vec![point0_id.into(), point1_id.into()],
11521 distance: Number {
11522 value: 2.0,
11523 units: NumericSuffix::Mm,
11524 },
11525 label_position: Some(label_position.clone()),
11526 source: Default::default(),
11527 });
11528 let (src_delta, scene_delta) = frontend
11529 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11530 .await
11531 .unwrap();
11532 assert_eq!(
11533 src_delta.text.as_str(),
11534 "\
11536sketch(on = XY) {
11537 point1 = point(at = [var 1, var 2])
11538 point2 = point(at = [var 3, var 4])
11539 horizontalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11540}
11541"
11542 );
11543 assert_eq!(
11544 scene_delta.new_graph.objects.len(),
11545 5,
11546 "{:#?}",
11547 scene_delta.new_graph.objects
11548 );
11549 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11550 let sketch = expect_sketch(sketch_object);
11551 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11552 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11553 panic!("Expected constraint object");
11554 };
11555 let Constraint::HorizontalDistance(distance) = constraint else {
11556 panic!("Expected horizontal distance constraint");
11557 };
11558 assert_eq!(distance.label_position, Some(label_position));
11559
11560 mock_ctx.close().await;
11561 }
11562
11563 #[tokio::test(flavor = "multi_thread")]
11564 async fn test_radius_single_arc_segment() {
11565 let initial_source = "\
11566sketch(on = XY) {
11567 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11568}
11569";
11570
11571 let program = Program::parse(initial_source).unwrap().0.unwrap();
11572
11573 let mut frontend = FrontendState::new();
11574
11575 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11576 let mock_ctx = ExecutorContext::new_mock(None).await;
11577 let version = Version(0);
11578
11579 frontend.hack_set_program(&ctx, program).await.unwrap();
11580 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11581 let sketch_id = sketch_object.id;
11582 let sketch = expect_sketch(sketch_object);
11583 let arc_id = sketch
11585 .segments
11586 .iter()
11587 .find(|&seg_id| {
11588 let obj = frontend.scene_graph.objects.get(seg_id.0);
11589 matches!(
11590 obj.map(|o| &o.kind),
11591 Some(ObjectKind::Segment {
11592 segment: Segment::Arc(_)
11593 })
11594 )
11595 })
11596 .unwrap();
11597
11598 let constraint = Constraint::Radius(Radius {
11599 arc: *arc_id,
11600 radius: Number {
11601 value: 5.0,
11602 units: NumericSuffix::Mm,
11603 },
11604 label_position: None,
11605 source: Default::default(),
11606 });
11607 let (src_delta, scene_delta) = frontend
11608 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11609 .await
11610 .unwrap();
11611 assert_eq!(
11612 src_delta.text.as_str(),
11613 "\
11615sketch(on = XY) {
11616 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11617 radius(arc1) == 5mm
11618}
11619"
11620 );
11621 assert_eq!(
11622 scene_delta.new_graph.objects.len(),
11623 7, "{:#?}",
11625 scene_delta.new_graph.objects
11626 );
11627
11628 ctx.close().await;
11629 mock_ctx.close().await;
11630 }
11631
11632 #[tokio::test(flavor = "multi_thread")]
11633 async fn test_radius_single_arc_segment_with_label_position() {
11634 let initial_source = "\
11635sketch(on = XY) {
11636 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
11637}
11638";
11639
11640 let program = Program::parse(initial_source).unwrap().0.unwrap();
11641 let mut frontend = FrontendState::new();
11642 let mock_ctx = ExecutorContext::new_mock(None).await;
11643 let version = Version(0);
11644
11645 frontend.program = program.clone();
11646 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11647 frontend.update_state_after_exec(outcome, true);
11648 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11649 let sketch_id = sketch_object.id;
11650 let sketch = expect_sketch(sketch_object);
11651 let arc_id = sketch
11652 .segments
11653 .iter()
11654 .find(|&seg_id| {
11655 let obj = frontend.scene_graph.objects.get(seg_id.0);
11656 matches!(
11657 obj.map(|o| &o.kind),
11658 Some(ObjectKind::Segment {
11659 segment: Segment::Arc(_)
11660 })
11661 )
11662 })
11663 .unwrap();
11664
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 let constraint = Constraint::Radius(Radius {
11676 arc: *arc_id,
11677 radius: Number {
11678 value: 5.0,
11679 units: NumericSuffix::Mm,
11680 },
11681 label_position: Some(label_position.clone()),
11682 source: Default::default(),
11683 });
11684 let (src_delta, scene_delta) = frontend
11685 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11686 .await
11687 .unwrap();
11688 assert_eq!(
11689 src_delta.text.as_str(),
11690 "\
11691sketch(on = XY) {
11692 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
11693 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11694}
11695"
11696 );
11697
11698 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11699 let sketch = expect_sketch(sketch_object);
11700 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11701 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11702 panic!("Expected constraint object");
11703 };
11704 let Constraint::Radius(radius) = constraint else {
11705 panic!("Expected radius constraint");
11706 };
11707 assert_eq!(radius.label_position, Some(label_position));
11708
11709 mock_ctx.close().await;
11710 }
11711
11712 #[tokio::test(flavor = "multi_thread")]
11713 async fn test_edit_radius_constraint_label_position() {
11714 let initial_source = "\
11715sketch(on = XY) {
11716 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11717 radius(arc1) == 5mm
11718}
11719";
11720
11721 let program = Program::parse(initial_source).unwrap().0.unwrap();
11722 let mut frontend = FrontendState::new();
11723 let mock_ctx = ExecutorContext::new_mock(None).await;
11724 let version = Version(0);
11725
11726 frontend.program = program.clone();
11727 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11728 frontend.update_state_after_exec(outcome, true);
11729 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11730 let sketch_id = sketch_object.id;
11731 let sketch = expect_sketch(sketch_object);
11732 let constraint_id = sketch.constraints[0];
11733 let label_position = Point2d {
11734 x: Number {
11735 value: 10.0,
11736 units: NumericSuffix::Mm,
11737 },
11738 y: Number {
11739 value: 11.0,
11740 units: NumericSuffix::Mm,
11741 },
11742 };
11743
11744 let (src_delta, scene_delta) = frontend
11745 .edit_distance_constraint_label_position(
11746 &mock_ctx,
11747 version,
11748 sketch_id,
11749 constraint_id,
11750 label_position.clone(),
11751 vec![],
11752 )
11753 .await
11754 .unwrap();
11755 assert_eq!(
11756 src_delta.text.as_str(),
11757 "\
11758sketch(on = XY) {
11759 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
11760 radius(arc1, labelPosition = [10mm, 11mm]) == 5mm
11761}
11762"
11763 );
11764
11765 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11766 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11767 panic!("Expected constraint object");
11768 };
11769 let Constraint::Radius(radius) = constraint else {
11770 panic!("Expected radius constraint");
11771 };
11772 assert_eq!(radius.label_position, Some(label_position));
11773
11774 mock_ctx.close().await;
11775 }
11776
11777 #[tokio::test(flavor = "multi_thread")]
11778 async fn test_vertical_distance_two_points() {
11779 let initial_source = "\
11780sketch(on = XY) {
11781 point(at = [var 1, var 2])
11782 point(at = [var 3, var 4])
11783}
11784";
11785
11786 let program = Program::parse(initial_source).unwrap().0.unwrap();
11787
11788 let mut frontend = FrontendState::new();
11789
11790 let mock_ctx = ExecutorContext::new_mock(None).await;
11791 let version = Version(0);
11792
11793 frontend.program = program.clone();
11794 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11795 frontend.update_state_after_exec(outcome, true);
11796 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11797 let sketch_id = sketch_object.id;
11798 let sketch = expect_sketch(sketch_object);
11799 let point0_id = *sketch.segments.first().unwrap();
11800 let point1_id = *sketch.segments.get(1).unwrap();
11801 let label_position = Point2d {
11802 x: Number {
11803 value: 10.0,
11804 units: NumericSuffix::Mm,
11805 },
11806 y: Number {
11807 value: 11.0,
11808 units: NumericSuffix::Mm,
11809 },
11810 };
11811
11812 let constraint = Constraint::VerticalDistance(Distance {
11813 points: vec![point0_id.into(), point1_id.into()],
11814 distance: Number {
11815 value: 2.0,
11816 units: NumericSuffix::Mm,
11817 },
11818 label_position: Some(label_position.clone()),
11819 source: Default::default(),
11820 });
11821 let (src_delta, scene_delta) = frontend
11822 .add_constraint(&mock_ctx, version, sketch_id, constraint)
11823 .await
11824 .unwrap();
11825 assert_eq!(
11826 src_delta.text.as_str(),
11827 "\
11829sketch(on = XY) {
11830 point1 = point(at = [var 1, var 2])
11831 point2 = point(at = [var 3, var 4])
11832 verticalDistance([point1, point2], labelPosition = [10mm, 11mm]) == 2mm
11833}
11834"
11835 );
11836 assert_eq!(
11837 scene_delta.new_graph.objects.len(),
11838 5,
11839 "{:#?}",
11840 scene_delta.new_graph.objects
11841 );
11842 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11843 let sketch = expect_sketch(sketch_object);
11844 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11845 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11846 panic!("Expected constraint object");
11847 };
11848 let Constraint::VerticalDistance(distance) = constraint else {
11849 panic!("Expected vertical distance constraint");
11850 };
11851 assert_eq!(distance.label_position, Some(label_position));
11852
11853 mock_ctx.close().await;
11854 }
11855
11856 #[tokio::test(flavor = "multi_thread")]
11857 async fn test_add_fixed_standalone_point() {
11858 let initial_source = "\
11859sketch(on = XY) {
11860 point(at = [var 1, var 2])
11861}
11862";
11863
11864 let program = Program::parse(initial_source).unwrap().0.unwrap();
11865
11866 let mut frontend = FrontendState::new();
11867
11868 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11869 let mock_ctx = ExecutorContext::new_mock(None).await;
11870 let version = Version(0);
11871
11872 frontend.hack_set_program(&ctx, program).await.unwrap();
11873 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11874 let sketch_id = sketch_object.id;
11875 let sketch = expect_sketch(sketch_object);
11876 let point_id = *sketch.segments.first().unwrap();
11877
11878 let (src_delta, scene_delta) = frontend
11879 .add_constraint(
11880 &mock_ctx,
11881 version,
11882 sketch_id,
11883 Constraint::Fixed(Fixed {
11884 points: vec![FixedPoint {
11885 point: point_id,
11886 position: Point2d {
11887 x: Number {
11888 value: 2.0,
11889 units: NumericSuffix::Mm,
11890 },
11891 y: Number {
11892 value: 3.0,
11893 units: NumericSuffix::Mm,
11894 },
11895 },
11896 }],
11897 }),
11898 )
11899 .await
11900 .unwrap();
11901 assert_eq!(
11902 src_delta.text.as_str(),
11903 "\
11904sketch(on = XY) {
11905 point1 = point(at = [var 2, var 3])
11906 fixed([point1, [2mm, 3mm]])
11907}
11908"
11909 );
11910 assert_eq!(
11911 scene_delta.new_graph.objects.len(),
11912 4,
11913 "{:#?}",
11914 scene_delta.new_graph.objects
11915 );
11916
11917 ctx.close().await;
11918 mock_ctx.close().await;
11919 }
11920
11921 #[tokio::test(flavor = "multi_thread")]
11922 async fn test_add_fixed_multiple_points() {
11923 let initial_source = "\
11924sketch(on = XY) {
11925 point(at = [var 1, var 2])
11926 point(at = [var 3, var 4])
11927}
11928";
11929
11930 let program = Program::parse(initial_source).unwrap().0.unwrap();
11931
11932 let mut frontend = FrontendState::new();
11933
11934 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
11935 let mock_ctx = ExecutorContext::new_mock(None).await;
11936 let version = Version(0);
11937
11938 frontend.hack_set_program(&ctx, program).await.unwrap();
11939 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11940 let sketch_id = sketch_object.id;
11941 let sketch = expect_sketch(sketch_object);
11942 let point0_id = *sketch.segments.first().unwrap();
11943 let point1_id = *sketch.segments.get(1).unwrap();
11944
11945 let (src_delta, scene_delta) = frontend
11946 .add_constraint(
11947 &mock_ctx,
11948 version,
11949 sketch_id,
11950 Constraint::Fixed(Fixed {
11951 points: vec![
11952 FixedPoint {
11953 point: point0_id,
11954 position: Point2d {
11955 x: Number {
11956 value: 2.0,
11957 units: NumericSuffix::Mm,
11958 },
11959 y: Number {
11960 value: 3.0,
11961 units: NumericSuffix::Mm,
11962 },
11963 },
11964 },
11965 FixedPoint {
11966 point: point1_id,
11967 position: Point2d {
11968 x: Number {
11969 value: 4.0,
11970 units: NumericSuffix::Mm,
11971 },
11972 y: Number {
11973 value: 5.0,
11974 units: NumericSuffix::Mm,
11975 },
11976 },
11977 },
11978 ],
11979 }),
11980 )
11981 .await
11982 .unwrap();
11983 assert_eq!(
11984 src_delta.text.as_str(),
11985 "\
11986sketch(on = XY) {
11987 point1 = point(at = [var 2, var 3])
11988 point2 = point(at = [var 4, var 5])
11989 fixed([point1, [2mm, 3mm]])
11990 fixed([point2, [4mm, 5mm]])
11991}
11992"
11993 );
11994 assert_eq!(
11995 scene_delta.new_graph.objects.len(),
11996 6,
11997 "{:#?}",
11998 scene_delta.new_graph.objects
11999 );
12000
12001 ctx.close().await;
12002 mock_ctx.close().await;
12003 }
12004
12005 #[tokio::test(flavor = "multi_thread")]
12006 async fn test_add_fixed_owned_point() {
12007 let initial_source = "\
12008sketch(on = XY) {
12009 line(start = [var 1, var 2], end = [var 3, var 4])
12010}
12011";
12012
12013 let program = Program::parse(initial_source).unwrap().0.unwrap();
12014
12015 let mut frontend = FrontendState::new();
12016
12017 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12018 let mock_ctx = ExecutorContext::new_mock(None).await;
12019 let version = Version(0);
12020
12021 frontend.hack_set_program(&ctx, program).await.unwrap();
12022 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12023 let sketch_id = sketch_object.id;
12024 let sketch = expect_sketch(sketch_object);
12025 let line_start_id = *sketch.segments.first().unwrap();
12026
12027 let (src_delta, scene_delta) = frontend
12028 .add_constraint(
12029 &mock_ctx,
12030 version,
12031 sketch_id,
12032 Constraint::Fixed(Fixed {
12033 points: vec![FixedPoint {
12034 point: line_start_id,
12035 position: Point2d {
12036 x: Number {
12037 value: 2.0,
12038 units: NumericSuffix::Mm,
12039 },
12040 y: Number {
12041 value: 3.0,
12042 units: NumericSuffix::Mm,
12043 },
12044 },
12045 }],
12046 }),
12047 )
12048 .await
12049 .unwrap();
12050 assert_eq!(
12051 src_delta.text.as_str(),
12052 "\
12053sketch(on = XY) {
12054 line1 = line(start = [var 2, var 3], end = [var 3, var 4])
12055 fixed([line1.start, [2mm, 3mm]])
12056}
12057"
12058 );
12059 assert_eq!(
12060 scene_delta.new_graph.objects.len(),
12061 6,
12062 "{:#?}",
12063 scene_delta.new_graph.objects
12064 );
12065
12066 ctx.close().await;
12067 mock_ctx.close().await;
12068 }
12069
12070 #[tokio::test(flavor = "multi_thread")]
12071 async fn test_radius_error_cases() {
12072 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12073 let mock_ctx = ExecutorContext::new_mock(None).await;
12074 let version = Version(0);
12075
12076 let initial_source_point = "\
12078sketch(on = XY) {
12079 point(at = [var 1, var 2])
12080}
12081";
12082 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12083 let mut frontend_point = FrontendState::new();
12084 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12085 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12086 let sketch_id_point = sketch_object_point.id;
12087 let sketch_point = expect_sketch(sketch_object_point);
12088 let point_id = *sketch_point.segments.first().unwrap();
12089
12090 let constraint_point = Constraint::Radius(Radius {
12091 arc: point_id,
12092 radius: Number {
12093 value: 5.0,
12094 units: NumericSuffix::Mm,
12095 },
12096 label_position: None,
12097 source: Default::default(),
12098 });
12099 let result_point = frontend_point
12100 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12101 .await;
12102 assert!(result_point.is_err(), "Single point should error for radius");
12103
12104 let initial_source_line = "\
12106sketch(on = XY) {
12107 line(start = [var 1, var 2], end = [var 3, var 4])
12108}
12109";
12110 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12111 let mut frontend_line = FrontendState::new();
12112 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12113 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12114 let sketch_id_line = sketch_object_line.id;
12115 let sketch_line = expect_sketch(sketch_object_line);
12116 let line_id = *sketch_line.segments.first().unwrap();
12117
12118 let constraint_line = Constraint::Radius(Radius {
12119 arc: line_id,
12120 radius: Number {
12121 value: 5.0,
12122 units: NumericSuffix::Mm,
12123 },
12124 label_position: None,
12125 source: Default::default(),
12126 });
12127 let result_line = frontend_line
12128 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12129 .await;
12130 assert!(result_line.is_err(), "Single line segment should error for radius");
12131
12132 ctx.close().await;
12133 mock_ctx.close().await;
12134 }
12135
12136 #[tokio::test(flavor = "multi_thread")]
12137 async fn test_diameter_single_arc_segment() {
12138 let initial_source = "\
12139sketch(on = XY) {
12140 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12141}
12142";
12143
12144 let program = Program::parse(initial_source).unwrap().0.unwrap();
12145
12146 let mut frontend = FrontendState::new();
12147
12148 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12149 let mock_ctx = ExecutorContext::new_mock(None).await;
12150 let version = Version(0);
12151
12152 frontend.hack_set_program(&ctx, program).await.unwrap();
12153 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12154 let sketch_id = sketch_object.id;
12155 let sketch = expect_sketch(sketch_object);
12156 let arc_id = sketch
12158 .segments
12159 .iter()
12160 .find(|&seg_id| {
12161 let obj = frontend.scene_graph.objects.get(seg_id.0);
12162 matches!(
12163 obj.map(|o| &o.kind),
12164 Some(ObjectKind::Segment {
12165 segment: Segment::Arc(_)
12166 })
12167 )
12168 })
12169 .unwrap();
12170
12171 let constraint = Constraint::Diameter(Diameter {
12172 arc: *arc_id,
12173 diameter: Number {
12174 value: 10.0,
12175 units: NumericSuffix::Mm,
12176 },
12177 label_position: None,
12178 source: Default::default(),
12179 });
12180 let (src_delta, scene_delta) = frontend
12181 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12182 .await
12183 .unwrap();
12184 assert_eq!(
12185 src_delta.text.as_str(),
12186 "\
12188sketch(on = XY) {
12189 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12190 diameter(arc1) == 10mm
12191}
12192"
12193 );
12194 assert_eq!(
12195 scene_delta.new_graph.objects.len(),
12196 7, "{:#?}",
12198 scene_delta.new_graph.objects
12199 );
12200
12201 ctx.close().await;
12202 mock_ctx.close().await;
12203 }
12204
12205 #[tokio::test(flavor = "multi_thread")]
12206 async fn test_diameter_single_arc_segment_with_label_position() {
12207 let initial_source = "\
12208sketch(on = XY) {
12209 arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12210}
12211";
12212
12213 let program = Program::parse(initial_source).unwrap().0.unwrap();
12214 let mut frontend = FrontendState::new();
12215 let mock_ctx = ExecutorContext::new_mock(None).await;
12216 let version = Version(0);
12217
12218 frontend.program = program.clone();
12219 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12220 frontend.update_state_after_exec(outcome, true);
12221 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12222 let sketch_id = sketch_object.id;
12223 let sketch = expect_sketch(sketch_object);
12224 let arc_id = sketch
12225 .segments
12226 .iter()
12227 .find(|&seg_id| {
12228 let obj = frontend.scene_graph.objects.get(seg_id.0);
12229 matches!(
12230 obj.map(|o| &o.kind),
12231 Some(ObjectKind::Segment {
12232 segment: Segment::Arc(_)
12233 })
12234 )
12235 })
12236 .unwrap();
12237
12238 let label_position = Point2d {
12239 x: Number {
12240 value: 10.0,
12241 units: NumericSuffix::Mm,
12242 },
12243 y: Number {
12244 value: 11.0,
12245 units: NumericSuffix::Mm,
12246 },
12247 };
12248 let constraint = Constraint::Diameter(Diameter {
12249 arc: *arc_id,
12250 diameter: Number {
12251 value: 10.0,
12252 units: NumericSuffix::Mm,
12253 },
12254 label_position: Some(label_position.clone()),
12255 source: Default::default(),
12256 });
12257 let (src_delta, scene_delta) = frontend
12258 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12259 .await
12260 .unwrap();
12261 assert_eq!(
12262 src_delta.text.as_str(),
12263 "\
12264sketch(on = XY) {
12265 arc1 = arc(start = [var 1.83, var 3.62], end = [var 2.42, var 3.3], center = [var -0.25, var -0.92])
12266 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12267}
12268"
12269 );
12270
12271 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12272 let sketch = expect_sketch(sketch_object);
12273 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12274 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12275 panic!("Expected constraint object");
12276 };
12277 let Constraint::Diameter(diameter) = constraint else {
12278 panic!("Expected diameter constraint");
12279 };
12280 assert_eq!(diameter.label_position, Some(label_position));
12281
12282 mock_ctx.close().await;
12283 }
12284
12285 #[tokio::test(flavor = "multi_thread")]
12286 async fn test_edit_diameter_constraint_label_position() {
12287 let initial_source = "\
12288sketch(on = XY) {
12289 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12290 diameter(arc1) == 10mm
12291}
12292";
12293
12294 let program = Program::parse(initial_source).unwrap().0.unwrap();
12295 let mut frontend = FrontendState::new();
12296 let mock_ctx = ExecutorContext::new_mock(None).await;
12297 let version = Version(0);
12298
12299 frontend.program = program.clone();
12300 let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12301 frontend.update_state_after_exec(outcome, true);
12302 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12303 let sketch_id = sketch_object.id;
12304 let sketch = expect_sketch(sketch_object);
12305 let constraint_id = sketch.constraints[0];
12306 let label_position = Point2d {
12307 x: Number {
12308 value: 10.0,
12309 units: NumericSuffix::Mm,
12310 },
12311 y: Number {
12312 value: 11.0,
12313 units: NumericSuffix::Mm,
12314 },
12315 };
12316
12317 let (src_delta, scene_delta) = frontend
12318 .edit_distance_constraint_label_position(
12319 &mock_ctx,
12320 version,
12321 sketch_id,
12322 constraint_id,
12323 label_position.clone(),
12324 vec![],
12325 )
12326 .await
12327 .unwrap();
12328 assert_eq!(
12329 src_delta.text.as_str(),
12330 "\
12331sketch(on = XY) {
12332 arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12333 diameter(arc1, labelPosition = [10mm, 11mm]) == 10mm
12334}
12335"
12336 );
12337
12338 let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12339 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12340 panic!("Expected constraint object");
12341 };
12342 let Constraint::Diameter(diameter) = constraint else {
12343 panic!("Expected diameter constraint");
12344 };
12345 assert_eq!(diameter.label_position, Some(label_position));
12346
12347 mock_ctx.close().await;
12348 }
12349
12350 #[tokio::test(flavor = "multi_thread")]
12351 async fn test_diameter_error_cases() {
12352 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12353 let mock_ctx = ExecutorContext::new_mock(None).await;
12354 let version = Version(0);
12355
12356 let initial_source_point = "\
12358sketch(on = XY) {
12359 point(at = [var 1, var 2])
12360}
12361";
12362 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12363 let mut frontend_point = FrontendState::new();
12364 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12365 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12366 let sketch_id_point = sketch_object_point.id;
12367 let sketch_point = expect_sketch(sketch_object_point);
12368 let point_id = *sketch_point.segments.first().unwrap();
12369
12370 let constraint_point = Constraint::Diameter(Diameter {
12371 arc: point_id,
12372 diameter: Number {
12373 value: 10.0,
12374 units: NumericSuffix::Mm,
12375 },
12376 label_position: None,
12377 source: Default::default(),
12378 });
12379 let result_point = frontend_point
12380 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12381 .await;
12382 assert!(result_point.is_err(), "Single point should error for diameter");
12383
12384 let initial_source_line = "\
12386sketch(on = XY) {
12387 line(start = [var 1, var 2], end = [var 3, var 4])
12388}
12389";
12390 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12391 let mut frontend_line = FrontendState::new();
12392 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12393 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12394 let sketch_id_line = sketch_object_line.id;
12395 let sketch_line = expect_sketch(sketch_object_line);
12396 let line_id = *sketch_line.segments.first().unwrap();
12397
12398 let constraint_line = Constraint::Diameter(Diameter {
12399 arc: line_id,
12400 diameter: Number {
12401 value: 10.0,
12402 units: NumericSuffix::Mm,
12403 },
12404 label_position: None,
12405 source: Default::default(),
12406 });
12407 let result_line = frontend_line
12408 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12409 .await;
12410 assert!(result_line.is_err(), "Single line segment should error for diameter");
12411
12412 ctx.close().await;
12413 mock_ctx.close().await;
12414 }
12415
12416 #[tokio::test(flavor = "multi_thread")]
12417 async fn test_line_horizontal() {
12418 let initial_source = "\
12419sketch(on = XY) {
12420 line(start = [var 1, var 2], end = [var 3, var 4])
12421}
12422";
12423
12424 let program = Program::parse(initial_source).unwrap().0.unwrap();
12425
12426 let mut frontend = FrontendState::new();
12427
12428 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12429 let mock_ctx = ExecutorContext::new_mock(None).await;
12430 let version = Version(0);
12431
12432 frontend.hack_set_program(&ctx, program).await.unwrap();
12433 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12434 let sketch_id = sketch_object.id;
12435 let sketch = expect_sketch(sketch_object);
12436 let line1_id = *sketch.segments.get(2).unwrap();
12437
12438 let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
12439 let (src_delta, scene_delta) = frontend
12440 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12441 .await
12442 .unwrap();
12443 assert_eq!(
12444 src_delta.text.as_str(),
12445 "\
12446sketch(on = XY) {
12447 line1 = line(start = [var 1, var 3], end = [var 3, var 3])
12448 horizontal(line1)
12449}
12450"
12451 );
12452 assert_eq!(
12453 scene_delta.new_graph.objects.len(),
12454 6,
12455 "{:#?}",
12456 scene_delta.new_graph.objects
12457 );
12458
12459 ctx.close().await;
12460 mock_ctx.close().await;
12461 }
12462
12463 #[tokio::test(flavor = "multi_thread")]
12464 async fn test_control_point_spline_edge_horizontal() {
12465 let initial_source = "\
12466@settings(experimentalFeatures = allow)
12467splineSketch = sketch(on = XY) {
12468 controlPointSpline1 = controlPointSpline(points = [
12469 [var 0mm, var 0mm],
12470 [var 10mm, var 20mm],
12471 [var 20mm, var 0mm],
12472 ])
12473}
12474";
12475
12476 let program = Program::parse(initial_source).unwrap().0.unwrap();
12477
12478 let mut frontend = FrontendState::new();
12479
12480 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12481 let mock_ctx = ExecutorContext::new_mock(None).await;
12482 let version = Version(0);
12483
12484 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12485 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12486 let sketch_id = sketch_object.id;
12487 let sketch = expect_sketch(sketch_object);
12488 let spline_id = sketch
12489 .segments
12490 .iter()
12491 .copied()
12492 .find(|seg_id| {
12493 matches!(
12494 &frontend.scene_graph.objects[seg_id.0].kind,
12495 ObjectKind::Segment {
12496 segment: Segment::ControlPointSpline(_)
12497 }
12498 )
12499 })
12500 .expect("Expected a control point spline segment in sketch");
12501 let edge_id = frontend
12502 .scene_graph
12503 .objects
12504 .iter()
12505 .find_map(|obj| match &obj.kind {
12506 ObjectKind::Segment {
12507 segment: Segment::Line(line),
12508 } if line.owner == Some(spline_id) => Some(obj.id),
12509 _ => None,
12510 })
12511 .expect("Expected an owned control-polygon edge");
12512
12513 let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
12514 let (src_delta, _) = frontend
12515 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12516 .await
12517 .unwrap();
12518 assert!(
12519 src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
12520 "Expected horizontal constraint on spline edge, got: {}",
12521 src_delta.text
12522 );
12523
12524 ctx.close().await;
12525 mock_ctx.close().await;
12526 }
12527
12528 #[tokio::test(flavor = "multi_thread")]
12529 async fn test_control_point_spline_edge_angle() {
12530 let initial_source = "\
12531@settings(experimentalFeatures = allow)
12532splineSketch = sketch(on = XY) {
12533 controlPointSpline1 = controlPointSpline(points = [
12534 [var 0mm, var 0mm],
12535 [var 10mm, var 20mm],
12536 [var 20mm, var 0mm],
12537 ])
12538
12539 line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
12540}
12541";
12542
12543 let program = Program::parse(initial_source).unwrap().0.unwrap();
12544
12545 let mut frontend = FrontendState::new();
12546
12547 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12548 let mock_ctx = ExecutorContext::new_mock(None).await;
12549 let version = Version(0);
12550
12551 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12552 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12553 let sketch_id = sketch_object.id;
12554 let sketch = expect_sketch(sketch_object);
12555 let spline_id = sketch
12556 .segments
12557 .iter()
12558 .copied()
12559 .find(|seg_id| {
12560 matches!(
12561 &frontend.scene_graph.objects[seg_id.0].kind,
12562 ObjectKind::Segment {
12563 segment: Segment::ControlPointSpline(_)
12564 }
12565 )
12566 })
12567 .expect("Expected a control point spline segment in sketch");
12568 let edge_id = frontend
12569 .scene_graph
12570 .objects
12571 .iter()
12572 .find_map(|obj| match &obj.kind {
12573 ObjectKind::Segment {
12574 segment: Segment::Line(line),
12575 } if line.owner == Some(spline_id) => Some(obj.id),
12576 _ => None,
12577 })
12578 .expect("Expected an owned control-polygon edge");
12579 let line1_id = frontend
12580 .scene_graph
12581 .objects
12582 .iter()
12583 .find_map(|obj| match &obj.kind {
12584 ObjectKind::Segment {
12585 segment: Segment::Line(line),
12586 } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
12587 _ => None,
12588 })
12589 .or_else(|| {
12590 sketch.segments.iter().copied().find(|seg_id| {
12591 matches!(
12592 &frontend.scene_graph.objects[seg_id.0].kind,
12593 ObjectKind::Segment {
12594 segment: Segment::Line(line),
12595 } if line.owner.is_none()
12596 )
12597 })
12598 })
12599 .expect("Expected a standalone line segment in sketch");
12600
12601 let constraint = Constraint::Angle(Angle {
12602 lines: vec![line1_id, edge_id],
12603 angle: Number {
12604 value: 30.0,
12605 units: NumericSuffix::Deg,
12606 },
12607 source: Default::default(),
12608 });
12609 let (src_delta, _) = frontend
12610 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12611 .await
12612 .unwrap();
12613 assert!(
12614 src_delta
12615 .text
12616 .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
12617 "Expected angle constraint on spline edge, got: {}",
12618 src_delta.text
12619 );
12620
12621 ctx.close().await;
12622 mock_ctx.close().await;
12623 }
12624
12625 #[tokio::test(flavor = "multi_thread")]
12626 async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
12627 let initial_source = "\
12628@settings(experimentalFeatures = allow)
12629splineSketch = sketch(on = XY) {
12630 spline1 = controlPointSpline(points = [
12631 [var 0mm, var 0mm],
12632 [var 10mm, var 20mm],
12633 [var 20mm, var 0mm],
12634 ])
12635 line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
12636 coincident([spline1.controls[1], spline1.edges[0]])
12637 coincident([spline1.controls[0], line1])
12638}
12639";
12640
12641 let program = Program::parse(initial_source).unwrap().0.unwrap();
12642
12643 let mut frontend = FrontendState::new();
12644
12645 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12646 let mock_ctx = ExecutorContext::new_mock(None).await;
12647
12648 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12649
12650 let ui_scene_graph = frontend.scene_graph_for_ui();
12651 let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
12652 let sketch = expect_sketch(sketch_object);
12653
12654 assert_eq!(
12655 sketch.constraints.len(),
12656 1,
12657 "Expected only the external coincident constraint to remain visible in the UI scene graph"
12658 );
12659
12660 let visible_constraints = ui_scene_graph
12661 .objects
12662 .iter()
12663 .filter_map(|object| match &object.kind {
12664 ObjectKind::Constraint {
12665 constraint: Constraint::Coincident(coincident),
12666 } => Some(coincident.clone()),
12667 _ => None,
12668 })
12669 .collect::<Vec<_>>();
12670
12671 assert_eq!(
12672 visible_constraints.len(),
12673 1,
12674 "Expected only one coincident constraint object in the UI scene graph"
12675 );
12676 assert_eq!(
12677 visible_constraints[0].get_segments().len(),
12678 2,
12679 "Expected the remaining visible coincident constraint to reference two segments"
12680 );
12681
12682 ctx.close().await;
12683 mock_ctx.close().await;
12684 }
12685
12686 #[tokio::test(flavor = "multi_thread")]
12687 async fn test_edit_control_point_spline_can_append_control_point() {
12688 let initial_source = "\
12689@settings(experimentalFeatures = allow)
12690splineSketch = sketch(on = XY) {
12691 controlPointSpline(points = [
12692 [var 0mm, var 0mm],
12693 [var 10mm, var 20mm],
12694 [var 20mm, var 0mm],
12695 ])
12696}
12697";
12698
12699 let program = Program::parse(initial_source).unwrap().0.unwrap();
12700
12701 let mut frontend = FrontendState::new();
12702
12703 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12704 let mock_ctx = ExecutorContext::new_mock(None).await;
12705 let version = Version(0);
12706
12707 seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
12708 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12709 let sketch_id = sketch_object.id;
12710 let sketch = expect_sketch(sketch_object);
12711 let spline_id = sketch
12712 .segments
12713 .iter()
12714 .copied()
12715 .find(|seg_id| {
12716 matches!(
12717 &frontend.scene_graph.objects[seg_id.0].kind,
12718 ObjectKind::Segment {
12719 segment: Segment::ControlPointSpline(_)
12720 }
12721 )
12722 })
12723 .expect("Expected a control point spline segment in sketch");
12724
12725 let ctor = ControlPointSplineCtor {
12726 points: vec![
12727 Point2d {
12728 x: Expr::Var(Number {
12729 value: 0.0,
12730 units: NumericSuffix::Mm,
12731 }),
12732 y: Expr::Var(Number {
12733 value: 0.0,
12734 units: NumericSuffix::Mm,
12735 }),
12736 },
12737 Point2d {
12738 x: Expr::Var(Number {
12739 value: 10.0,
12740 units: NumericSuffix::Mm,
12741 }),
12742 y: Expr::Var(Number {
12743 value: 20.0,
12744 units: NumericSuffix::Mm,
12745 }),
12746 },
12747 Point2d {
12748 x: Expr::Var(Number {
12749 value: 20.0,
12750 units: NumericSuffix::Mm,
12751 }),
12752 y: Expr::Var(Number {
12753 value: 0.0,
12754 units: NumericSuffix::Mm,
12755 }),
12756 },
12757 Point2d {
12758 x: Expr::Var(Number {
12759 value: 30.0,
12760 units: NumericSuffix::Mm,
12761 }),
12762 y: Expr::Var(Number {
12763 value: 10.0,
12764 units: NumericSuffix::Mm,
12765 }),
12766 },
12767 ],
12768 construction: None,
12769 };
12770
12771 let segments = vec![ExistingSegmentCtor {
12772 id: spline_id,
12773 ctor: SegmentCtor::ControlPointSpline(ctor),
12774 }];
12775 let (src_delta, scene_delta) = frontend
12776 .edit_segments(&mock_ctx, version, sketch_id, segments)
12777 .await
12778 .unwrap();
12779
12780 assert!(
12781 src_delta.text.contains("[var 30mm, var 10mm]"),
12782 "Expected appended spline control point in source, got: {}",
12783 src_delta.text
12784 );
12785
12786 assert!(
12787 scene_delta.invalidates_ids,
12788 "Expected appending a spline control point to invalidate ids"
12789 );
12790 let updated_spline = scene_delta
12791 .new_graph
12792 .objects
12793 .iter()
12794 .find_map(|obj| match &obj.kind {
12795 ObjectKind::Segment {
12796 segment: Segment::ControlPointSpline(updated_spline),
12797 } if updated_spline.controls.len() == 4 => Some(updated_spline),
12798 _ => None,
12799 })
12800 .expect("Expected edited scene graph to contain a four-point control point spline");
12801 assert_eq!(
12802 updated_spline.controls.len(),
12803 4,
12804 "Expected edited spline to expose four control points"
12805 );
12806
12807 ctx.close().await;
12808 mock_ctx.close().await;
12809 }
12810
12811 #[tokio::test(flavor = "multi_thread")]
12812 async fn test_line_vertical() {
12813 let initial_source = "\
12814sketch(on = XY) {
12815 line(start = [var 1, var 2], end = [var 3, var 4])
12816}
12817";
12818
12819 let program = Program::parse(initial_source).unwrap().0.unwrap();
12820
12821 let mut frontend = FrontendState::new();
12822
12823 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12824 let mock_ctx = ExecutorContext::new_mock(None).await;
12825 let version = Version(0);
12826
12827 frontend.hack_set_program(&ctx, program).await.unwrap();
12828 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12829 let sketch_id = sketch_object.id;
12830 let sketch = expect_sketch(sketch_object);
12831 let line1_id = *sketch.segments.get(2).unwrap();
12832
12833 let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
12834 let (src_delta, scene_delta) = frontend
12835 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12836 .await
12837 .unwrap();
12838 assert_eq!(
12839 src_delta.text.as_str(),
12840 "\
12841sketch(on = XY) {
12842 line1 = line(start = [var 2, var 2], end = [var 2, var 4])
12843 vertical(line1)
12844}
12845"
12846 );
12847 assert_eq!(
12848 scene_delta.new_graph.objects.len(),
12849 6,
12850 "{:#?}",
12851 scene_delta.new_graph.objects
12852 );
12853
12854 ctx.close().await;
12855 mock_ctx.close().await;
12856 }
12857
12858 #[tokio::test(flavor = "multi_thread")]
12859 async fn test_points_vertical() {
12860 let initial_source = "\
12861sketch001 = sketch(on = XY) {
12862 p0 = point(at = [var -2.23mm, var 3.1mm])
12863 pf = point(at = [4, 4])
12864}
12865";
12866
12867 let program = Program::parse(initial_source).unwrap().0.unwrap();
12868
12869 let mut frontend = FrontendState::new();
12870
12871 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12872 let mock_ctx = ExecutorContext::new_mock(None).await;
12873 let version = Version(0);
12874
12875 frontend.hack_set_program(&ctx, program).await.unwrap();
12876 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12877 let sketch_id = sketch_object.id;
12878 let sketch = expect_sketch(sketch_object);
12879 let point_ids = vec![
12880 sketch.segments.first().unwrap().to_owned(),
12881 sketch.segments.get(1).unwrap().to_owned(),
12882 ];
12883
12884 let constraint = Constraint::Vertical(Vertical::Points {
12885 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12886 });
12887 let (src_delta, scene_delta) = frontend
12888 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12889 .await
12890 .unwrap();
12891 assert_eq!(
12892 src_delta.text.as_str(),
12893 "\
12894sketch001 = sketch(on = XY) {
12895 p0 = point(at = [var 4mm, var 3.1mm])
12896 pf = point(at = [4, 4])
12897 vertical([p0, pf])
12898}
12899"
12900 );
12901 assert_eq!(
12902 scene_delta.new_graph.objects.len(),
12903 5,
12904 "{:#?}",
12905 scene_delta.new_graph.objects
12906 );
12907
12908 ctx.close().await;
12909 mock_ctx.close().await;
12910 }
12911
12912 #[tokio::test(flavor = "multi_thread")]
12913 async fn test_points_horizontal() {
12914 let initial_source = "\
12915sketch001 = sketch(on = XY) {
12916 p0 = point(at = [var -2.23mm, var 3.1mm])
12917 pf = point(at = [4, 4])
12918}
12919";
12920
12921 let program = Program::parse(initial_source).unwrap().0.unwrap();
12922
12923 let mut frontend = FrontendState::new();
12924
12925 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12926 let mock_ctx = ExecutorContext::new_mock(None).await;
12927 let version = Version(0);
12928
12929 frontend.hack_set_program(&ctx, program).await.unwrap();
12930 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12931 let sketch_id = sketch_object.id;
12932 let sketch = expect_sketch(sketch_object);
12933 let point_ids = vec![
12934 sketch.segments.first().unwrap().to_owned(),
12935 sketch.segments.get(1).unwrap().to_owned(),
12936 ];
12937
12938 let constraint = Constraint::Horizontal(Horizontal::Points {
12939 points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
12940 });
12941 let (src_delta, scene_delta) = frontend
12942 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12943 .await
12944 .unwrap();
12945 assert_eq!(
12946 src_delta.text.as_str(),
12947 "\
12948sketch001 = sketch(on = XY) {
12949 p0 = point(at = [var -2.23mm, var 4mm])
12950 pf = point(at = [4, 4])
12951 horizontal([p0, pf])
12952}
12953"
12954 );
12955 assert_eq!(
12956 scene_delta.new_graph.objects.len(),
12957 5,
12958 "{:#?}",
12959 scene_delta.new_graph.objects
12960 );
12961
12962 ctx.close().await;
12963 mock_ctx.close().await;
12964 }
12965
12966 #[tokio::test(flavor = "multi_thread")]
12967 async fn test_point_horizontal_with_origin() {
12968 let initial_source = "\
12969sketch001 = sketch(on = XY) {
12970 p0 = point(at = [var -2.23mm, var 3.1mm])
12971}
12972";
12973
12974 let program = Program::parse(initial_source).unwrap().0.unwrap();
12975
12976 let mut frontend = FrontendState::new();
12977
12978 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
12979 let mock_ctx = ExecutorContext::new_mock(None).await;
12980 let version = Version(0);
12981
12982 frontend.hack_set_program(&ctx, program).await.unwrap();
12983 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12984 let sketch_id = sketch_object.id;
12985 let sketch = expect_sketch(sketch_object);
12986 let point_id = *sketch.segments.first().unwrap();
12987
12988 let constraint = Constraint::Horizontal(Horizontal::Points {
12989 points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
12990 });
12991 let (src_delta, scene_delta) = frontend
12992 .add_constraint(&mock_ctx, version, sketch_id, constraint)
12993 .await
12994 .unwrap();
12995 assert_eq!(
12996 src_delta.text.as_str(),
12997 "\
12998sketch001 = sketch(on = XY) {
12999 p0 = point(at = [var -2.23mm, var 0mm])
13000 horizontal([p0, ORIGIN])
13001}
13002"
13003 );
13004 assert_eq!(
13005 scene_delta.new_graph.objects.len(),
13006 4,
13007 "{:#?}",
13008 scene_delta.new_graph.objects
13009 );
13010
13011 ctx.close().await;
13012 mock_ctx.close().await;
13013 }
13014
13015 #[tokio::test(flavor = "multi_thread")]
13016 async fn test_lines_equal_length() {
13017 let initial_source = "\
13018sketch(on = XY) {
13019 line(start = [var 1, var 2], end = [var 3, var 4])
13020 line(start = [var 5, var 6], end = [var 7, var 8])
13021}
13022";
13023
13024 let program = Program::parse(initial_source).unwrap().0.unwrap();
13025
13026 let mut frontend = FrontendState::new();
13027
13028 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13029 let mock_ctx = ExecutorContext::new_mock(None).await;
13030 let version = Version(0);
13031
13032 frontend.hack_set_program(&ctx, program).await.unwrap();
13033 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13034 let sketch_id = sketch_object.id;
13035 let sketch = expect_sketch(sketch_object);
13036 let line1_id = *sketch.segments.get(2).unwrap();
13037 let line2_id = *sketch.segments.get(5).unwrap();
13038
13039 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13040 lines: vec![line1_id, line2_id],
13041 });
13042 let (src_delta, scene_delta) = frontend
13043 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13044 .await
13045 .unwrap();
13046 assert_eq!(
13047 src_delta.text.as_str(),
13048 "\
13049sketch(on = XY) {
13050 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13051 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13052 equalLength([line1, line2])
13053}
13054"
13055 );
13056 assert_eq!(
13057 scene_delta.new_graph.objects.len(),
13058 9,
13059 "{:#?}",
13060 scene_delta.new_graph.objects
13061 );
13062
13063 ctx.close().await;
13064 mock_ctx.close().await;
13065 }
13066
13067 #[tokio::test(flavor = "multi_thread")]
13068 async fn test_add_constraint_multi_line_equal_length() {
13069 let initial_source = "\
13070sketch(on = XY) {
13071 line(start = [var 1, var 2], end = [var 3, var 4])
13072 line(start = [var 5, var 6], end = [var 7, var 8])
13073 line(start = [var 9, var 10], end = [var 11, var 12])
13074}
13075";
13076
13077 let program = Program::parse(initial_source).unwrap().0.unwrap();
13078
13079 let mut frontend = FrontendState::new();
13080 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13081 let mock_ctx = ExecutorContext::new_mock(None).await;
13082 let version = Version(0);
13083
13084 frontend.hack_set_program(&ctx, program).await.unwrap();
13085 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13086 let sketch_id = sketch_object.id;
13087 let sketch = expect_sketch(sketch_object);
13088 let line1_id = *sketch.segments.get(2).unwrap();
13089 let line2_id = *sketch.segments.get(5).unwrap();
13090 let line3_id = *sketch.segments.get(8).unwrap();
13091
13092 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13093 lines: vec![line1_id, line2_id, line3_id],
13094 });
13095 let (src_delta, scene_delta) = frontend
13096 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13097 .await
13098 .unwrap();
13099 assert_eq!(
13100 src_delta.text.as_str(),
13101 "\
13102sketch(on = XY) {
13103 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13104 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13105 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
13106 equalLength([line1, line2, line3])
13107}
13108"
13109 );
13110 let constraints = scene_delta
13111 .new_graph
13112 .objects
13113 .iter()
13114 .filter_map(|obj| {
13115 let ObjectKind::Constraint { constraint } = &obj.kind else {
13116 return None;
13117 };
13118 Some(constraint)
13119 })
13120 .collect::<Vec<_>>();
13121
13122 assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13123 let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13124 panic!("expected equal length constraint, got {:?}", constraints[0]);
13125 };
13126 assert_eq!(lines_equal_length.lines.len(), 3);
13127
13128 ctx.close().await;
13129 mock_ctx.close().await;
13130 }
13131
13132 #[tokio::test(flavor = "multi_thread")]
13133 async fn test_lines_parallel() {
13134 let initial_source = "\
13135sketch(on = XY) {
13136 line(start = [var 1, var 2], end = [var 3, var 4])
13137 line(start = [var 5, var 6], end = [var 7, var 8])
13138}
13139";
13140
13141 let program = Program::parse(initial_source).unwrap().0.unwrap();
13142
13143 let mut frontend = FrontendState::new();
13144
13145 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13146 let mock_ctx = ExecutorContext::new_mock(None).await;
13147 let version = Version(0);
13148
13149 frontend.hack_set_program(&ctx, program).await.unwrap();
13150 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13151 let sketch_id = sketch_object.id;
13152 let sketch = expect_sketch(sketch_object);
13153 let line1_id = *sketch.segments.get(2).unwrap();
13154 let line2_id = *sketch.segments.get(5).unwrap();
13155
13156 let constraint = Constraint::Parallel(Parallel {
13157 lines: vec![line1_id, line2_id],
13158 });
13159 let (src_delta, scene_delta) = frontend
13160 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13161 .await
13162 .unwrap();
13163 assert_eq!(
13164 src_delta.text.as_str(),
13165 "\
13166sketch(on = XY) {
13167 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13168 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13169 parallel([line1, line2])
13170}
13171"
13172 );
13173 assert_eq!(
13174 scene_delta.new_graph.objects.len(),
13175 9,
13176 "{:#?}",
13177 scene_delta.new_graph.objects
13178 );
13179
13180 ctx.close().await;
13181 mock_ctx.close().await;
13182 }
13183
13184 #[tokio::test(flavor = "multi_thread")]
13185 async fn test_lines_parallel_multiline() {
13186 let initial_source = "\
13187sketch(on = XY) {
13188 line(start = [var 1, var 2], end = [var 3, var 4])
13189 line(start = [var 5, var 6], end = [var 7, var 8])
13190 line(start = [var 9, var 10], end = [var 11, var 12])
13191}
13192";
13193
13194 let program = Program::parse(initial_source).unwrap().0.unwrap();
13195
13196 let mut frontend = FrontendState::new();
13197
13198 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13199 let mock_ctx = ExecutorContext::new_mock(None).await;
13200 let version = Version(0);
13201
13202 frontend.hack_set_program(&ctx, program).await.unwrap();
13203 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13204 let sketch_id = sketch_object.id;
13205 let sketch = expect_sketch(sketch_object);
13206 let line1_id = *sketch.segments.get(2).unwrap();
13207 let line2_id = *sketch.segments.get(5).unwrap();
13208 let line3_id = *sketch.segments.get(8).unwrap();
13209
13210 let constraint = Constraint::Parallel(Parallel {
13211 lines: vec![line1_id, line2_id, line3_id],
13212 });
13213 let (src_delta, scene_delta) = frontend
13214 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13215 .await
13216 .unwrap();
13217 assert_eq!(
13218 src_delta.text.as_str(),
13219 "\
13220sketch(on = XY) {
13221 line1 = line(start = [var 1, var 2], end = [var 3, var 4])
13222 line2 = line(start = [var 5, var 6], end = [var 7, var 8])
13223 line3 = line(start = [var 9, var 10], end = [var 11, var 12])
13224 parallel([line1, line2, line3])
13225}
13226"
13227 );
13228
13229 let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13230 let sketch = expect_sketch(sketch_object);
13231 assert_eq!(sketch.constraints.len(), 1);
13232
13233 let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13234 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13235 panic!("Expected constraint object");
13236 };
13237 let Constraint::Parallel(parallel) = constraint else {
13238 panic!("Expected parallel constraint");
13239 };
13240 assert_eq!(parallel.lines.len(), 3);
13241
13242 ctx.close().await;
13243 mock_ctx.close().await;
13244 }
13245
13246 #[tokio::test(flavor = "multi_thread")]
13247 async fn test_lines_perpendicular() {
13248 let initial_source = "\
13249sketch(on = XY) {
13250 line(start = [var 1, var 2], end = [var 3, var 4])
13251 line(start = [var 5, var 6], end = [var 7, var 8])
13252}
13253";
13254
13255 let program = Program::parse(initial_source).unwrap().0.unwrap();
13256
13257 let mut frontend = FrontendState::new();
13258
13259 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13260 let mock_ctx = ExecutorContext::new_mock(None).await;
13261 let version = Version(0);
13262
13263 frontend.hack_set_program(&ctx, program).await.unwrap();
13264 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13265 let sketch_id = sketch_object.id;
13266 let sketch = expect_sketch(sketch_object);
13267 let line1_id = *sketch.segments.get(2).unwrap();
13268 let line2_id = *sketch.segments.get(5).unwrap();
13269
13270 let constraint = Constraint::Perpendicular(Perpendicular {
13271 lines: vec![line1_id, line2_id],
13272 });
13273 let (src_delta, scene_delta) = frontend
13274 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13275 .await
13276 .unwrap();
13277 assert_eq!(
13278 src_delta.text.as_str(),
13279 "\
13280sketch(on = XY) {
13281 line1 = line(start = [var 2, var 3], end = [var 2, var 3])
13282 line2 = line(start = [var 6, var 7], end = [var 6, var 7])
13283 perpendicular([line1, line2])
13284}
13285"
13286 );
13287 assert_eq!(
13288 scene_delta.new_graph.objects.len(),
13289 9,
13290 "{:#?}",
13291 scene_delta.new_graph.objects
13292 );
13293
13294 ctx.close().await;
13295 mock_ctx.close().await;
13296 }
13297
13298 #[tokio::test(flavor = "multi_thread")]
13299 async fn test_lines_angle() {
13300 let initial_source = "\
13301sketch(on = XY) {
13302 line(start = [var 1, var 2], end = [var 3, var 4])
13303 line(start = [var 5, var 6], end = [var 7, var 8])
13304}
13305";
13306
13307 let program = Program::parse(initial_source).unwrap().0.unwrap();
13308
13309 let mut frontend = FrontendState::new();
13310
13311 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13312 let mock_ctx = ExecutorContext::new_mock(None).await;
13313 let version = Version(0);
13314
13315 frontend.hack_set_program(&ctx, program).await.unwrap();
13316 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13317 let sketch_id = sketch_object.id;
13318 let sketch = expect_sketch(sketch_object);
13319 let line1_id = *sketch.segments.get(2).unwrap();
13320 let line2_id = *sketch.segments.get(5).unwrap();
13321
13322 let constraint = Constraint::Angle(Angle {
13323 lines: vec![line1_id, line2_id],
13324 angle: Number {
13325 value: 30.0,
13326 units: NumericSuffix::Deg,
13327 },
13328 source: Default::default(),
13329 });
13330 let (src_delta, scene_delta) = frontend
13331 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13332 .await
13333 .unwrap();
13334 assert_eq!(
13335 src_delta.text.as_str(),
13336 "\
13338sketch(on = XY) {
13339 line1 = line(start = [var 0.9, var 2.36], end = [var 3.1, var 3.64])
13340 line2 = line(start = [var 5.36, var 5.9], end = [var 6.64, var 8.1])
13341 angle([line1, line2]) == 30deg
13342}
13343"
13344 );
13345 assert_eq!(
13346 scene_delta.new_graph.objects.len(),
13347 9,
13348 "{:#?}",
13349 scene_delta.new_graph.objects
13350 );
13351
13352 ctx.close().await;
13353 mock_ctx.close().await;
13354 }
13355
13356 #[tokio::test(flavor = "multi_thread")]
13357 async fn test_segments_tangent() {
13358 let initial_source = "\
13359sketch(on = XY) {
13360 line(start = [var 1, var 2], end = [var 3, var 4])
13361 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13362}
13363";
13364
13365 let program = Program::parse(initial_source).unwrap().0.unwrap();
13366
13367 let mut frontend = FrontendState::new();
13368
13369 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13370 let mock_ctx = ExecutorContext::new_mock(None).await;
13371 let version = Version(0);
13372
13373 frontend.hack_set_program(&ctx, program).await.unwrap();
13374 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13375 let sketch_id = sketch_object.id;
13376 let sketch = expect_sketch(sketch_object);
13377 let line1_id = *sketch.segments.get(2).unwrap();
13378 let arc1_id = *sketch.segments.get(6).unwrap();
13379
13380 let constraint = Constraint::Tangent(Tangent {
13381 input: vec![line1_id, arc1_id],
13382 });
13383 let (src_delta, scene_delta) = frontend
13384 .add_constraint(&mock_ctx, version, sketch_id, constraint)
13385 .await
13386 .unwrap();
13387 assert_eq!(
13388 src_delta.text.as_str(),
13389 "\
13390sketch(on = XY) {
13391 line1 = line(start = [var 0.84, var 2.13], end = [var 3.82, var 3.27])
13392 arc1 = arc(start = [var 4.51, var 2.03], end = [var 7.05, var 2.02], center = [var 5.78, var 2.55])
13393 tangent([line1, arc1])
13394}
13395"
13396 );
13397 assert_eq!(
13398 scene_delta.new_graph.objects.len(),
13399 10,
13400 "{:#?}",
13401 scene_delta.new_graph.objects
13402 );
13403
13404 ctx.close().await;
13405 mock_ctx.close().await;
13406 }
13407
13408 #[tokio::test(flavor = "multi_thread")]
13409 async fn test_point_midpoint() {
13410 let initial_source = "\
13411sketch(on = XY) {
13412 point(at = [var 1, var 1])
13413 line(start = [var 0, var 0], end = [var 6, var 4])
13414}
13415";
13416
13417 let program = Program::parse(initial_source).unwrap().0.unwrap();
13418
13419 let mut frontend = FrontendState::new();
13420
13421 let ctx = ExecutorContext::new_mock(None).await;
13422 let version = Version(0);
13423
13424 frontend.program = program.clone();
13425 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13426 frontend.update_state_after_exec(outcome, true);
13427 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13428 let sketch_id = sketch_object.id;
13429 let sketch = expect_sketch(sketch_object);
13430 let point_id = *sketch.segments.first().unwrap();
13431 let line_id = *sketch.segments.get(3).unwrap();
13432
13433 let constraint = Constraint::Midpoint(Midpoint {
13434 point: ConstraintSegment::from(point_id),
13435 segment: line_id,
13436 });
13437 let (src_delta, scene_delta) = frontend
13438 .add_constraint(&ctx, version, sketch_id, constraint)
13439 .await
13440 .unwrap();
13441 assert_eq!(
13442 src_delta.text.as_str(),
13443 "\
13444sketch(on = XY) {
13445 point1 = point(at = [var 2.33, var 1.67])
13446 line1 = line(start = [var -0.67, var -0.33], end = [var 5.33, var 3.67])
13447 midpoint(line1, point = point1)
13448}
13449"
13450 );
13451 assert_eq!(
13452 scene_delta.new_graph.objects.len(),
13453 7,
13454 "{:#?}",
13455 scene_delta.new_graph.objects
13456 );
13457
13458 ctx.close().await;
13459 }
13460
13461 #[tokio::test(flavor = "multi_thread")]
13462 async fn test_segments_symmetric() {
13463 let initial_source = "\
13464sketch(on = XY) {
13465 line(start = [var 0, var 0], end = [var 0, var 4])
13466 line(start = [var 4, var 0], end = [var 4, var 4])
13467 line(start = [var 2, var -1], end = [var 2, var 5])
13468}
13469";
13470
13471 let program = Program::parse(initial_source).unwrap().0.unwrap();
13472
13473 let mut frontend = FrontendState::new();
13474
13475 let ctx = ExecutorContext::new_mock(None).await;
13476 let version = Version(0);
13477
13478 frontend.program = program.clone();
13479 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13480 frontend.update_state_after_exec(outcome, true);
13481 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13482 let sketch_id = sketch_object.id;
13483 let sketch = expect_sketch(sketch_object);
13484 let line1_id = *sketch.segments.get(2).unwrap();
13485 let line2_id = *sketch.segments.get(5).unwrap();
13486 let axis_id = *sketch.segments.get(8).unwrap();
13487
13488 let constraint = Constraint::Symmetric(Symmetric {
13489 input: vec![line1_id, line2_id],
13490 axis: axis_id,
13491 });
13492 let (src_delta, scene_delta) = frontend
13493 .add_constraint(&ctx, version, sketch_id, constraint)
13494 .await
13495 .unwrap();
13496 assert_eq!(
13497 src_delta.text.as_str(),
13498 "\
13499sketch(on = XY) {
13500 line1 = line(start = [var 0, var 0], end = [var 0, var 4])
13501 line2 = line(start = [var 4, var 0], end = [var 4, var 4])
13502 line3 = line(start = [var 2, var -1], end = [var 2, var 5])
13503 symmetric([line1, line2], axis = line3)
13504}
13505"
13506 );
13507 assert_eq!(
13508 scene_delta.new_graph.objects.len(),
13509 12,
13510 "{:#?}",
13511 scene_delta.new_graph.objects
13512 );
13513
13514 ctx.close().await;
13515 }
13516
13517 #[tokio::test(flavor = "multi_thread")]
13518 async fn test_point_arc_midpoint() {
13519 let initial_source = "\
13520sketch(on = XY) {
13521 point(at = [var 6, var 3])
13522 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13523}
13524";
13525
13526 let program = Program::parse(initial_source).unwrap().0.unwrap();
13527
13528 let mut frontend = FrontendState::new();
13529
13530 let ctx = ExecutorContext::new_mock(None).await;
13531 let version = Version(0);
13532
13533 frontend.program = program.clone();
13534 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13535 frontend.update_state_after_exec(outcome, true);
13536 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13537 let sketch_id = sketch_object.id;
13538 let sketch = expect_sketch(sketch_object);
13539 let point_id = *sketch.segments.first().unwrap();
13540 let arc_id = *sketch.segments.get(4).unwrap();
13541
13542 let constraint = Constraint::Midpoint(Midpoint {
13543 point: ConstraintSegment::from(point_id),
13544 segment: arc_id,
13545 });
13546 let (src_delta, scene_delta) = frontend
13547 .add_constraint(&ctx, version, sketch_id, constraint)
13548 .await
13549 .unwrap();
13550 assert_eq!(
13551 src_delta.text.as_str(),
13552 "\
13553sketch(on = XY) {
13554 point1 = point(at = [var 6, var 2.35])
13555 arc1 = arc(start = [var 6, var 2.35], end = [var 6, var 2.35], center = [var 6, var 1.94])
13556 midpoint(arc1, point = point1)
13557}
13558"
13559 );
13560 assert_eq!(
13561 scene_delta.new_graph.objects.len(),
13562 8,
13563 "{:#?}",
13564 scene_delta.new_graph.objects
13565 );
13566
13567 ctx.close().await;
13568 }
13569
13570 #[tokio::test(flavor = "multi_thread")]
13571 async fn test_origin_line_midpoint() {
13572 let initial_source = "\
13573sketch(on = XY) {
13574 line(start = [var 0, var 0], end = [var 6, var 4])
13575}
13576";
13577
13578 let program = Program::parse(initial_source).unwrap().0.unwrap();
13579
13580 let mut frontend = FrontendState::new();
13581
13582 let ctx = ExecutorContext::new_mock(None).await;
13583 let version = Version(0);
13584
13585 frontend.program = program.clone();
13586 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13587 frontend.update_state_after_exec(outcome, true);
13588 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13589 let sketch_id = sketch_object.id;
13590 let sketch = expect_sketch(sketch_object);
13591 let line_id = *sketch.segments.get(2).unwrap();
13592
13593 let constraint = Constraint::Midpoint(Midpoint {
13594 point: ConstraintSegment::ORIGIN,
13595 segment: line_id,
13596 });
13597 let (src_delta, scene_delta) = frontend
13598 .add_constraint(&ctx, version, sketch_id, constraint)
13599 .await
13600 .unwrap();
13601 assert_eq!(
13602 src_delta.text.as_str(),
13603 "\
13604sketch(on = XY) {
13605 line1 = line(start = [var -3, var -2], end = [var 3, var 2])
13606 midpoint(line1, point = ORIGIN)
13607}
13608"
13609 );
13610 assert_eq!(
13611 scene_delta.new_graph.objects.len(),
13612 6,
13613 "{:#?}",
13614 scene_delta.new_graph.objects
13615 );
13616
13617 ctx.close().await;
13618 }
13619
13620 #[tokio::test(flavor = "multi_thread")]
13621 async fn test_origin_arc_midpoint() {
13622 let initial_source = "\
13623sketch(on = XY) {
13624 arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13625}
13626";
13627
13628 let program = Program::parse(initial_source).unwrap().0.unwrap();
13629
13630 let mut frontend = FrontendState::new();
13631
13632 let ctx = ExecutorContext::new_mock(None).await;
13633 let version = Version(0);
13634
13635 frontend.program = program.clone();
13636 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13637 frontend.update_state_after_exec(outcome, true);
13638 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13639 let sketch_id = sketch_object.id;
13640 let sketch = expect_sketch(sketch_object);
13641 let arc_id = *sketch.segments.get(3).unwrap();
13642
13643 let constraint = Constraint::Midpoint(Midpoint {
13644 point: ConstraintSegment::ORIGIN,
13645 segment: arc_id,
13646 });
13647 let (src_delta, scene_delta) = frontend
13648 .add_constraint(&ctx, version, sketch_id, constraint)
13649 .await
13650 .unwrap();
13651 assert_eq!(
13652 src_delta.text.as_str(),
13653 "\
13654sketch(on = XY) {
13655 arc1 = arc(start = [var 0.35, var 2.24], end = [var 1.62, var -1.58], center = [var 2.34, var 0.78])
13656 midpoint(arc1, point = ORIGIN)
13657}
13658"
13659 );
13660 assert_eq!(
13661 scene_delta.new_graph.objects.len(),
13662 7,
13663 "{:#?}",
13664 scene_delta.new_graph.objects
13665 );
13666
13667 ctx.close().await;
13668 }
13669
13670 #[tokio::test(flavor = "multi_thread")]
13671 async fn test_segments_symmetric_arcs() {
13672 let initial_source = "\
13673sketch(on = XY) {
13674 arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
13675 arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
13676 line(start = [var 0, var -10], end = [var 0, var 10])
13677}
13678";
13679
13680 let program = Program::parse(initial_source).unwrap().0.unwrap();
13681
13682 let mut frontend = FrontendState::new();
13683
13684 let ctx = ExecutorContext::new_mock(None).await;
13685 let version = Version(0);
13686
13687 frontend.program = program.clone();
13688 let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13689 frontend.update_state_after_exec(outcome, true);
13690 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13691 let sketch_id = sketch_object.id;
13692 let sketch = expect_sketch(sketch_object);
13693 let arc1_id = *sketch.segments.get(3).unwrap();
13694 let arc2_id = *sketch.segments.get(7).unwrap();
13695 let axis_id = *sketch.segments.get(10).unwrap();
13696
13697 let constraint = Constraint::Symmetric(Symmetric {
13698 input: vec![arc1_id, arc2_id],
13699 axis: axis_id,
13700 });
13701 let (src_delta, scene_delta) = frontend
13702 .add_constraint(&ctx, version, sketch_id, constraint)
13703 .await
13704 .unwrap();
13705 assert_eq!(
13706 src_delta.text.as_str(),
13707 "\
13708sketch(on = XY) {
13709 arc1 = arc(start = [var -14.46, var 0], end = [var -10, var 4.65], center = [var -10.14, var 0.31])
13710 arc2 = arc(start = [var 5.49, var 2.26], end = [var 11.58, var -3.47], center = [var 9.34, var 0.25])
13711 line1 = line(start = [var -0.44, var -10], end = [var -0.37, var 10])
13712 symmetric([arc1, arc2], axis = line1)
13713}
13714"
13715 );
13716 assert_eq!(
13717 scene_delta.new_graph.objects.len(),
13718 14,
13719 "{:#?}",
13720 scene_delta.new_graph.objects
13721 );
13722
13723 ctx.close().await;
13724 }
13725
13726 #[tokio::test(flavor = "multi_thread")]
13727 async fn test_sketch_on_face_simple() {
13728 let initial_source = "\
13729len = 2mm
13730cube = startSketchOn(XY)
13731 |> startProfile(at = [0, 0])
13732 |> line(end = [len, 0], tag = $side)
13733 |> line(end = [0, len])
13734 |> line(end = [-len, 0])
13735 |> line(end = [0, -len])
13736 |> close()
13737 |> extrude(length = len)
13738
13739face = faceOf(cube, face = side)
13740";
13741
13742 let program = Program::parse(initial_source).unwrap().0.unwrap();
13743
13744 let mut frontend = FrontendState::new();
13745
13746 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13747 let mock_ctx = ExecutorContext::new_mock(None).await;
13748 let version = Version(0);
13749
13750 frontend.hack_set_program(&ctx, program).await.unwrap();
13751 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
13752 let face_id = face_object.id;
13753
13754 let sketch_args = SketchCtor {
13755 on: Plane::Object(face_id),
13756 };
13757 let (_src_delta, scene_delta, sketch_id) = frontend
13758 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13759 .await
13760 .unwrap();
13761 assert_eq!(sketch_id, ObjectId(2));
13762 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
13763 let sketch_object = &scene_delta.new_graph.objects[2];
13764 assert_eq!(sketch_object.id, ObjectId(2));
13765 assert_eq!(
13766 sketch_object.kind,
13767 ObjectKind::Sketch(Sketch {
13768 args: SketchCtor {
13769 on: Plane::Object(face_id),
13770 },
13771 plane: face_id,
13772 segments: vec![],
13773 constraints: vec![],
13774 })
13775 );
13776 assert_eq!(scene_delta.new_graph.objects.len(), 8);
13777
13778 ctx.close().await;
13779 mock_ctx.close().await;
13780 }
13781
13782 #[tokio::test(flavor = "multi_thread")]
13783 async fn test_sketch_on_wall_artifact_from_region_extrude() {
13784 let initial_source = "\
13785s = sketch(on = YZ) {
13786 line1 = line(start = [0, 0], end = [0, 1])
13787 line2 = line(start = [0, 1], end = [1, 1])
13788 line3 = line(start = [1, 1], end = [0, 0])
13789}
13790region001 = region(point = [0.1, 0.1], sketch = s)
13791extrude001 = extrude(region001, length = 5)
13792";
13793
13794 let program = Program::parse(initial_source).unwrap().0.unwrap();
13795
13796 let mut frontend = FrontendState::new();
13797 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13798 let version = Version(0);
13799
13800 frontend.hack_set_program(&ctx, program).await.unwrap();
13801 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13802
13803 let sketch_args = SketchCtor {
13804 on: Plane::Object(wall_object_id),
13805 };
13806 let (src_delta, _scene_delta, _sketch_id) = frontend
13807 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13808 .await
13809 .unwrap();
13810 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13811
13812 ctx.close().await;
13813 }
13814
13815 #[tokio::test(flavor = "multi_thread")]
13816 async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
13817 let initial_source = "\
13818sketch001 = sketch(on = YZ) {
13819 line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
13820 line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
13821 line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
13822 line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
13823 coincident([line1.end, line2.start])
13824 coincident([line2.end, line3.start])
13825 coincident([line3.end, line4.start])
13826 coincident([line4.end, line1.start])
13827 parallel([line2, line4])
13828 parallel([line3, line1])
13829 perpendicular([line1, line2])
13830 horizontal(line3)
13831 line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
13832}
13833region001 = region(point = [3.1, 3.74], sketch = sketch001)
13834extrude001 = extrude(region001, length = 5)
13835";
13836
13837 let program = Program::parse(initial_source).unwrap().0.unwrap();
13838
13839 let mut frontend = FrontendState::new();
13840 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13841 let version = Version(0);
13842
13843 frontend.hack_set_program(&ctx, program).await.unwrap();
13844 let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
13845
13846 let sketch_args = SketchCtor {
13847 on: Plane::Object(wall_object_id),
13848 };
13849 let (src_delta, _scene_delta, _sketch_id) = frontend
13850 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
13851 .await
13852 .unwrap();
13853 assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
13854
13855 ctx.close().await;
13856 }
13857
13858 #[test]
13859 fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
13860 let source = "\
13861sketch001 = sketch(on = XY) {
13862 line(start = [0, 0], end = [1, 0])
13863}
13864part = subtract(boxSolid, tools = [cutSolid])
13865 |> appearance(color = \"#8f96a3\")
13866";
13867 let ast = Program::parse(source).unwrap().0.unwrap().ast;
13868 let line_start = source.find("line").unwrap();
13869 let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
13870 let line_ref = SourceRef::Simple {
13871 range: [line_start, line_end, 0].into(),
13872 node_path: None,
13873 };
13874 assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
13875
13876 let subtract_start = source.find("subtract").unwrap();
13877 let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
13878 let subtract_ref = SourceRef::Simple {
13879 range: [subtract_start, subtract_end, 0].into(),
13880 node_path: None,
13881 };
13882 assert_eq!(
13883 variable_name_containing_source_ref(&ast, &subtract_ref),
13884 Some("part".to_owned())
13885 );
13886 }
13887
13888 #[tokio::test(flavor = "multi_thread")]
13889 async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
13890 clear_mem_cache().await;
13891 let source = "\
13892boxSolid = startSketchOn(XY)
13893 |> startProfile(at = [0, 0])
13894 |> line(end = [4, 0], tag = $bottomEdge)
13895 |> line(end = [0, 4])
13896 |> line(end = [-4, 0])
13897 |> close()
13898 |> extrude(length = 10)
13899cutSolid = startSketchOn(XY)
13900 |> startProfile(at = [1, 1])
13901 |> line(end = [1, 0])
13902 |> line(end = [0, 1])
13903 |> line(end = [-1, 0])
13904 |> close()
13905 |> extrude(length = 10)
13906part = subtract(boxSolid, tools = [cutSolid])
13907 |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
13908";
13909 let program = Program::parse(source).unwrap().0.unwrap();
13910 let mut frontend = FrontendState::new();
13911 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13912 match frontend.hack_set_program(&ctx, program).await.unwrap() {
13913 SetProgramOutcome::Success { .. } => {}
13914 SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
13915 }
13916
13917 let sweep_call_start = source.find("extrude").unwrap();
13918 let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
13919 let part_call_start = source.find("subtract").unwrap();
13920 let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
13921 let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
13922 let composite_range = [part_call_start, part_call_end, 0].into();
13923
13924 let cap_object = frontend
13925 .scene_graph
13926 .objects
13927 .iter()
13928 .find(|object| {
13929 matches!(
13930 object.kind,
13931 ObjectKind::Cap(crate::frontend::api::Cap {
13932 kind: crate::frontend::api::CapKind::End,
13933 ..
13934 })
13935 ) && matches!(
13936 &object.source,
13937 SourceRef::BackTrace { ranges }
13938 if ranges.len() == 2 && ranges[0].0 == composite_range && ranges[1].0 == sweep_range
13939 )
13940 })
13941 .expect("expected end cap object to trace through subtract and original extrude");
13942
13943 let mut ast = frontend.program.ast.clone();
13944 let cap_expr = sketch_on_ast_expr(&mut ast, &frontend.scene_graph, &Plane::Object(cap_object.id)).unwrap();
13945 let cap_face_decl = ast::VariableDeclaration::new(
13946 ast::VariableDeclarator::new("capFace", cap_expr.clone()),
13947 ast::ItemVisibility::Default,
13948 ast::VariableKind::Const,
13949 );
13950 ast.body
13951 .push(ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(
13952 cap_face_decl,
13953 ))));
13954 let generated_source = source_from_ast(&ast);
13955
13956 assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
13957 assert!(!generated_source.contains("faceOf(boxSolid"));
13958 let ast::Expr::CallExpressionKw(call) = cap_expr else {
13959 panic!("expected faceOf call");
13960 };
13961 assert_eq!(call.callee.name.name, "faceOf");
13962 let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
13963 panic!("expected solid name");
13964 };
13965 assert_eq!(solid_name.name.name, "part");
13966 let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
13967 panic!("expected face name");
13968 };
13969 assert_eq!(face_name.name.name, "END");
13970
13971 ctx.close().await;
13972 }
13973
13974 #[tokio::test(flavor = "multi_thread")]
13975 async fn test_sketch_on_plane_incremental() {
13976 let initial_source = "\
13977len = 2mm
13978cube = startSketchOn(XY)
13979 |> startProfile(at = [0, 0])
13980 |> line(end = [len, 0], tag = $side)
13981 |> line(end = [0, len])
13982 |> line(end = [-len, 0])
13983 |> line(end = [0, -len])
13984 |> close()
13985 |> extrude(length = len)
13986
13987plane = planeOf(cube, face = side)
13988";
13989
13990 let program = Program::parse(initial_source).unwrap().0.unwrap();
13991
13992 let mut frontend = FrontendState::new();
13993
13994 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
13995 let mock_ctx = ExecutorContext::new_mock(None).await;
13996 let version = Version(0);
13997
13998 frontend.hack_set_program(&ctx, program).await.unwrap();
13999 let plane_object = frontend
14001 .scene_graph
14002 .objects
14003 .iter()
14004 .rev()
14005 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14006 .unwrap();
14007 let plane_id = plane_object.id;
14008
14009 let sketch_args = SketchCtor {
14010 on: Plane::Object(plane_id),
14011 };
14012 let (src_delta, scene_delta, sketch_id) = frontend
14013 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14014 .await
14015 .unwrap();
14016 assert_eq!(
14017 src_delta.text.as_str(),
14018 "\
14019len = 2mm
14020cube = startSketchOn(XY)
14021 |> startProfile(at = [0, 0])
14022 |> line(end = [len, 0], tag = $side)
14023 |> line(end = [0, len])
14024 |> line(end = [-len, 0])
14025 |> line(end = [0, -len])
14026 |> close()
14027 |> extrude(length = len)
14028
14029plane = planeOf(cube, face = side)
14030sketch001 = sketch(on = plane) {
14031}
14032"
14033 );
14034 assert_eq!(sketch_id, ObjectId(2));
14035 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14036 let sketch_object = &scene_delta.new_graph.objects[2];
14037 assert_eq!(sketch_object.id, ObjectId(2));
14038 assert_eq!(
14039 sketch_object.kind,
14040 ObjectKind::Sketch(Sketch {
14041 args: SketchCtor {
14042 on: Plane::Object(plane_id),
14043 },
14044 plane: plane_id,
14045 segments: vec![],
14046 constraints: vec![],
14047 })
14048 );
14049 assert_eq!(scene_delta.new_graph.objects.len(), 9);
14050
14051 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14052 assert_eq!(plane_object.id, plane_id);
14053 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14054
14055 ctx.close().await;
14056 mock_ctx.close().await;
14057 }
14058
14059 #[tokio::test(flavor = "multi_thread")]
14060 async fn test_new_sketch_uses_unique_variable_name() {
14061 let initial_source = "\
14062sketch1 = sketch(on = XY) {
14063}
14064";
14065
14066 let program = Program::parse(initial_source).unwrap().0.unwrap();
14067
14068 let mut frontend = FrontendState::new();
14069 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14070 let version = Version(0);
14071
14072 frontend.hack_set_program(&ctx, program).await.unwrap();
14073
14074 let sketch_args = SketchCtor {
14075 on: Plane::Default(PlaneName::Yz),
14076 };
14077 let (src_delta, _, _) = frontend
14078 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14079 .await
14080 .unwrap();
14081
14082 assert_eq!(
14083 src_delta.text.as_str(),
14084 "\
14085sketch1 = sketch(on = XY) {
14086}
14087sketch001 = sketch(on = YZ) {
14088}
14089"
14090 );
14091
14092 ctx.close().await;
14093 }
14094
14095 #[tokio::test(flavor = "multi_thread")]
14096 async fn test_new_sketch_twice_using_same_plane() {
14097 let initial_source = "\
14098sketch1 = sketch(on = XY) {
14099}
14100";
14101
14102 let program = Program::parse(initial_source).unwrap().0.unwrap();
14103
14104 let mut frontend = FrontendState::new();
14105 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14106 let version = Version(0);
14107
14108 frontend.hack_set_program(&ctx, program).await.unwrap();
14109
14110 let sketch_args = SketchCtor {
14111 on: Plane::Default(PlaneName::Xy),
14112 };
14113 let (src_delta, _, _) = frontend
14114 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14115 .await
14116 .unwrap();
14117
14118 assert_eq!(
14119 src_delta.text.as_str(),
14120 "\
14121sketch1 = sketch(on = XY) {
14122}
14123sketch001 = sketch(on = XY) {
14124}
14125"
14126 );
14127
14128 ctx.close().await;
14129 }
14130
14131 #[tokio::test(flavor = "multi_thread")]
14132 async fn test_sketch_mode_reuses_cached_on_expression() {
14133 let initial_source = "\
14134width = 2mm
14135sketch(on = offsetPlane(XY, offset = width)) {
14136 line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14137 distance([line1.start, line1.end]) == width
14138}
14139";
14140 let program = Program::parse(initial_source).unwrap().0.unwrap();
14141
14142 let mut frontend = FrontendState::new();
14143 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14144 let mock_ctx = ExecutorContext::new_mock(None).await;
14145 let version = Version(0);
14146 let project_id = ProjectId(0);
14147 let file_id = FileId(0);
14148
14149 frontend.hack_set_program(&ctx, program).await.unwrap();
14150 let initial_object_count = frontend.scene_graph.objects.len();
14151 let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14152 .expect("Expected sketch object to exist")
14153 .id;
14154
14155 let scene_delta = frontend
14158 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14159 .await
14160 .unwrap();
14161 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14162
14163 let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14166 assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14167
14168 ctx.close().await;
14169 mock_ctx.close().await;
14170 }
14171
14172 #[tokio::test(flavor = "multi_thread")]
14173 async fn test_multiple_sketch_blocks() {
14174 let initial_source = "\
14175// Cube that requires the engine.
14176width = 2
14177sketch001 = startSketchOn(XY)
14178profile001 = startProfile(sketch001, at = [0, 0])
14179 |> yLine(length = width, tag = $seg1)
14180 |> xLine(length = width)
14181 |> yLine(length = -width)
14182 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14183 |> close()
14184extrude001 = extrude(profile001, length = width)
14185
14186// Get a value that requires the engine.
14187x = segLen(seg1)
14188
14189// Triangle with side length 2*x.
14190sketch(on = XY) {
14191 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14192 line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14193 coincident([line1.end, line2.start])
14194 line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14195 coincident([line2.end, line3.start])
14196 coincident([line3.end, line1.start])
14197 equalLength([line3, line1])
14198 equalLength([line1, line2])
14199 distance([line1.start, line1.end]) == 2*x
14200}
14201
14202// Line segment with length x.
14203sketch2 = sketch(on = XY) {
14204 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14205 distance([line1.start, line1.end]) == x
14206}
14207";
14208
14209 let program = Program::parse(initial_source).unwrap().0.unwrap();
14210
14211 let mut frontend = FrontendState::new();
14212
14213 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14214 let mock_ctx = ExecutorContext::new_mock(None).await;
14215 let version = Version(0);
14216 let project_id = ProjectId(0);
14217 let file_id = FileId(0);
14218
14219 frontend.hack_set_program(&ctx, program).await.unwrap();
14220 let sketch_objects = frontend
14221 .scene_graph
14222 .objects
14223 .iter()
14224 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14225 .collect::<Vec<_>>();
14226 let sketch1_id = sketch_objects.first().unwrap().id;
14227 let sketch2_id = sketch_objects.get(1).unwrap().id;
14228 let point1_id = ObjectId(sketch1_id.0 + 1);
14230 let point2_id = ObjectId(sketch2_id.0 + 1);
14232
14233 let scene_delta = frontend
14242 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14243 .await
14244 .unwrap();
14245 assert_eq!(
14246 scene_delta.new_graph.objects.len(),
14247 18,
14248 "{:#?}",
14249 scene_delta.new_graph.objects
14250 );
14251
14252 let point_ctor = PointCtor {
14254 position: Point2d {
14255 x: Expr::Var(Number {
14256 value: 1.0,
14257 units: NumericSuffix::Mm,
14258 }),
14259 y: Expr::Var(Number {
14260 value: 2.0,
14261 units: NumericSuffix::Mm,
14262 }),
14263 },
14264 };
14265 let segments = vec![ExistingSegmentCtor {
14266 id: point1_id,
14267 ctor: SegmentCtor::Point(point_ctor),
14268 }];
14269 let (src_delta, _) = frontend
14270 .edit_segments(&mock_ctx, version, sketch1_id, segments)
14271 .await
14272 .unwrap();
14273 assert_eq!(
14275 src_delta.text.as_str(),
14276 "\
14277// Cube that requires the engine.
14278width = 2
14279sketch001 = startSketchOn(XY)
14280profile001 = startProfile(sketch001, at = [0, 0])
14281 |> yLine(length = width, tag = $seg1)
14282 |> xLine(length = width)
14283 |> yLine(length = -width)
14284 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14285 |> close()
14286extrude001 = extrude(profile001, length = width)
14287
14288// Get a value that requires the engine.
14289x = segLen(seg1)
14290
14291// Triangle with side length 2*x.
14292sketch(on = XY) {
14293 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14294 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14295 coincident([line1.end, line2.start])
14296 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14297 coincident([line2.end, line3.start])
14298 coincident([line3.end, line1.start])
14299 equalLength([line3, line1])
14300 equalLength([line1, line2])
14301 distance([line1.start, line1.end]) == 2 * x
14302}
14303
14304// Line segment with length x.
14305sketch2 = sketch(on = XY) {
14306 line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14307 distance([line1.start, line1.end]) == x
14308}
14309"
14310 );
14311 let edited_sketch1_source = src_delta.text.clone();
14312
14313 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
14315 assert_eq!(src_delta.text, edited_sketch1_source);
14316 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14324 assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
14325
14326 let scene_delta = frontend
14334 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14335 .await
14336 .unwrap();
14337 assert_eq!(
14338 scene_delta.new_graph.objects.len(),
14339 24,
14340 "{:#?}",
14341 scene_delta.new_graph.objects
14342 );
14343
14344 let point_ctor = PointCtor {
14346 position: Point2d {
14347 x: Expr::Var(Number {
14348 value: 3.0,
14349 units: NumericSuffix::Mm,
14350 }),
14351 y: Expr::Var(Number {
14352 value: 4.0,
14353 units: NumericSuffix::Mm,
14354 }),
14355 },
14356 };
14357 let segments = vec![ExistingSegmentCtor {
14358 id: point2_id,
14359 ctor: SegmentCtor::Point(point_ctor),
14360 }];
14361 let (src_delta, _) = frontend
14362 .edit_segments(&mock_ctx, version, sketch2_id, segments)
14363 .await
14364 .unwrap();
14365 assert_eq!(
14367 src_delta.text.as_str(),
14368 "\
14369// Cube that requires the engine.
14370width = 2
14371sketch001 = startSketchOn(XY)
14372profile001 = startProfile(sketch001, at = [0, 0])
14373 |> yLine(length = width, tag = $seg1)
14374 |> xLine(length = width)
14375 |> yLine(length = -width)
14376 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14377 |> close()
14378extrude001 = extrude(profile001, length = width)
14379
14380// Get a value that requires the engine.
14381x = segLen(seg1)
14382
14383// Triangle with side length 2*x.
14384sketch(on = XY) {
14385 line1 = line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
14386 line2 = line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
14387 coincident([line1.end, line2.start])
14388 line3 = line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
14389 coincident([line2.end, line3.start])
14390 coincident([line3.end, line1.start])
14391 equalLength([line3, line1])
14392 equalLength([line1, line2])
14393 distance([line1.start, line1.end]) == 2 * x
14394}
14395
14396// Line segment with length x.
14397sketch2 = sketch(on = XY) {
14398 line1 = line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
14399 distance([line1.start, line1.end]) == x
14400}
14401"
14402 );
14403 let edited_sketch2_source = src_delta.text.clone();
14404
14405 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
14407 assert_eq!(src_delta.text, edited_sketch2_source);
14408
14409 ctx.close().await;
14410 mock_ctx.close().await;
14411 }
14412
14413 #[tokio::test(flavor = "multi_thread")]
14414 async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
14415 clear_mem_cache().await;
14416
14417 let source = r#"sketch001 = sketch(on = XZ) {
14418 circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
14419}
14420sketch002 = sketch(on = XY) {
14421 line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
14422 line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
14423 line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
14424 line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
14425 coincident([line1.end, line2.start])
14426 coincident([line2.end, line3.start])
14427 coincident([line3.end, line4.start])
14428 coincident([line4.end, line1.start])
14429 parallel([line2, line4])
14430 parallel([line3, line1])
14431 perpendicular([line1, line2])
14432 horizontal(line3)
14433 coincident([line1.start, ORIGIN])
14434}
14435"#;
14436
14437 let program = Program::parse(source).unwrap().0.unwrap();
14438 let mut frontend = FrontendState::new();
14439 let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
14440 let mock_ctx = ExecutorContext::new_mock(None).await;
14441 let version = Version(0);
14442 let project_id = ProjectId(0);
14443 let file_id = FileId(0);
14444
14445 frontend.hack_set_program(&ctx, program).await.unwrap();
14446 let sketch_objects = frontend
14447 .scene_graph
14448 .objects
14449 .iter()
14450 .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
14451 .collect::<Vec<_>>();
14452 assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
14453
14454 let sketch1_id = sketch_objects[0].id;
14455 let sketch2_id = sketch_objects[1].id;
14456
14457 frontend
14458 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
14459 .await
14460 .unwrap();
14461 frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
14462
14463 let scene_delta = frontend
14464 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
14465 .await
14466 .unwrap();
14467 assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
14468
14469 clear_mem_cache().await;
14470 ctx.close().await;
14471 mock_ctx.close().await;
14472 }
14473
14474 #[tokio::test(flavor = "multi_thread")]
14479 async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
14480 let initial_source = "@settings(defaultLengthUnit = mm)
14482
14483
14484
14485sketch001 = sketch(on = XY) {
14486 point(at = [1in, 2in])
14487}
14488";
14489
14490 let program = Program::parse(initial_source).unwrap().0.unwrap();
14491 let mut frontend = FrontendState::new();
14492
14493 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14494 let mock_ctx = ExecutorContext::new_mock(None).await;
14495 let version = Version(0);
14496 let project_id = ProjectId(0);
14497 let file_id = FileId(0);
14498
14499 frontend.hack_set_program(&ctx, program).await.unwrap();
14500 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14501 let sketch_id = sketch_object.id;
14502
14503 frontend
14505 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14506 .await
14507 .unwrap();
14508
14509 let point_ctor = PointCtor {
14511 position: Point2d {
14512 x: Expr::Number(Number {
14513 value: 5.0,
14514 units: NumericSuffix::Mm,
14515 }),
14516 y: Expr::Number(Number {
14517 value: 6.0,
14518 units: NumericSuffix::Mm,
14519 }),
14520 },
14521 };
14522 let segment = SegmentCtor::Point(point_ctor);
14523 let (src_delta, scene_delta) = frontend
14524 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14525 .await
14526 .unwrap();
14527 assert!(
14529 src_delta.text.contains("point(at = [5mm, 6mm])"),
14530 "Expected new point in source, got: {}",
14531 src_delta.text
14532 );
14533 assert!(!scene_delta.new_objects.is_empty());
14534
14535 ctx.close().await;
14536 mock_ctx.close().await;
14537 }
14538
14539 #[tokio::test(flavor = "multi_thread")]
14540 async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
14541 let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
14542
14543 let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
14544 let meta_settings = updated_program.meta_settings().unwrap().unwrap();
14545
14546 assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
14547 assert!(
14548 source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
14549 "Expected experimental settings to be added to source"
14550 );
14551 }
14552
14553 #[tokio::test(flavor = "multi_thread")]
14554 async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
14555 let initial_source = "@settings(defaultLengthUnit = mm)
14557
14558
14559
14560s = sketch(on = XY) {}
14561";
14562
14563 let program = Program::parse(initial_source).unwrap().0.unwrap();
14564 let mut frontend = FrontendState::new();
14565
14566 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14567 let mock_ctx = ExecutorContext::new_mock(None).await;
14568 let version = Version(0);
14569
14570 frontend.hack_set_program(&ctx, program).await.unwrap();
14571 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14572 let sketch_id = sketch_object.id;
14573
14574 let line_ctor = LineCtor {
14575 start: Point2d {
14576 x: Expr::Number(Number {
14577 value: 0.0,
14578 units: NumericSuffix::Mm,
14579 }),
14580 y: Expr::Number(Number {
14581 value: 0.0,
14582 units: NumericSuffix::Mm,
14583 }),
14584 },
14585 end: Point2d {
14586 x: Expr::Number(Number {
14587 value: 10.0,
14588 units: NumericSuffix::Mm,
14589 }),
14590 y: Expr::Number(Number {
14591 value: 10.0,
14592 units: NumericSuffix::Mm,
14593 }),
14594 },
14595 construction: None,
14596 };
14597 let segment = SegmentCtor::Line(line_ctor);
14598 let (src_delta, scene_delta) = frontend
14599 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14600 .await
14601 .unwrap();
14602 assert!(
14603 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
14604 "Expected line in source, got: {}",
14605 src_delta.text
14606 );
14607 assert_eq!(scene_delta.new_objects.len(), 3);
14609
14610 ctx.close().await;
14611 mock_ctx.close().await;
14612 }
14613
14614 #[tokio::test(flavor = "multi_thread")]
14615 async fn test_extra_newlines_between_operations_edit_line() {
14616 let initial_source = "@settings(defaultLengthUnit = mm)
14618
14619
14620sketch001 = sketch(on = XY) {
14621
14622 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14623
14624}
14625";
14626
14627 let program = Program::parse(initial_source).unwrap().0.unwrap();
14628 let mut frontend = FrontendState::new();
14629
14630 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14631 let mock_ctx = ExecutorContext::new_mock(None).await;
14632 let version = Version(0);
14633 let project_id = ProjectId(0);
14634 let file_id = FileId(0);
14635
14636 frontend.hack_set_program(&ctx, program).await.unwrap();
14637 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14638 let sketch_id = sketch_object.id;
14639 let sketch = expect_sketch(sketch_object);
14640
14641 let line_id = sketch
14643 .segments
14644 .iter()
14645 .copied()
14646 .find(|seg_id| {
14647 matches!(
14648 &frontend.scene_graph.objects[seg_id.0].kind,
14649 ObjectKind::Segment {
14650 segment: Segment::Line(_)
14651 }
14652 )
14653 })
14654 .expect("Expected a line segment in sketch");
14655
14656 frontend
14658 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14659 .await
14660 .unwrap();
14661
14662 let line_ctor = LineCtor {
14664 start: Point2d {
14665 x: Expr::Var(Number {
14666 value: 1.0,
14667 units: NumericSuffix::Mm,
14668 }),
14669 y: Expr::Var(Number {
14670 value: 2.0,
14671 units: NumericSuffix::Mm,
14672 }),
14673 },
14674 end: Point2d {
14675 x: Expr::Var(Number {
14676 value: 13.0,
14677 units: NumericSuffix::Mm,
14678 }),
14679 y: Expr::Var(Number {
14680 value: 14.0,
14681 units: NumericSuffix::Mm,
14682 }),
14683 },
14684 construction: None,
14685 };
14686 let segments = vec![ExistingSegmentCtor {
14687 id: line_id,
14688 ctor: SegmentCtor::Line(line_ctor),
14689 }];
14690 let (src_delta, _scene_delta) = frontend
14691 .edit_segments(&mock_ctx, version, sketch_id, segments)
14692 .await
14693 .unwrap();
14694 assert!(
14695 src_delta
14696 .text
14697 .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
14698 "Expected edited line in source, got: {}",
14699 src_delta.text
14700 );
14701
14702 ctx.close().await;
14703 mock_ctx.close().await;
14704 }
14705
14706 #[tokio::test(flavor = "multi_thread")]
14707 async fn test_extra_newlines_delete_segment() {
14708 let initial_source = "@settings(defaultLengthUnit = mm)
14710
14711
14712
14713sketch001 = sketch(on = XY) {
14714 circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14715}
14716";
14717
14718 let program = Program::parse(initial_source).unwrap().0.unwrap();
14719 let mut frontend = FrontendState::new();
14720
14721 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14722 let mock_ctx = ExecutorContext::new_mock(None).await;
14723 let version = Version(0);
14724
14725 frontend.hack_set_program(&ctx, program).await.unwrap();
14726 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14727 let sketch_id = sketch_object.id;
14728 let sketch = expect_sketch(sketch_object);
14729
14730 assert_eq!(sketch.segments.len(), 3);
14732 let circle_id = sketch.segments[2];
14733
14734 let (src_delta, scene_delta) = frontend
14736 .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
14737 .await
14738 .unwrap();
14739 assert!(
14740 src_delta.text.contains("sketch(on = XY) {"),
14741 "Expected sketch block in source, got: {}",
14742 src_delta.text
14743 );
14744 let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
14745 let new_sketch = expect_sketch(new_sketch_object);
14746 assert_eq!(new_sketch.segments.len(), 0);
14747
14748 ctx.close().await;
14749 mock_ctx.close().await;
14750 }
14751
14752 #[tokio::test(flavor = "multi_thread")]
14753 async fn test_unformatted_source_add_arc() {
14754 let initial_source = "@settings(defaultLengthUnit = mm)
14756
14757
14758
14759
14760sketch001 = sketch(on = XY) {
14761}
14762";
14763
14764 let program = Program::parse(initial_source).unwrap().0.unwrap();
14765 let mut frontend = FrontendState::new();
14766
14767 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14768 let mock_ctx = ExecutorContext::new_mock(None).await;
14769 let version = Version(0);
14770
14771 frontend.hack_set_program(&ctx, program).await.unwrap();
14772 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14773 let sketch_id = sketch_object.id;
14774
14775 let arc_ctor = ArcCtor {
14776 start: Point2d {
14777 x: Expr::Var(Number {
14778 value: 5.0,
14779 units: NumericSuffix::Mm,
14780 }),
14781 y: Expr::Var(Number {
14782 value: 0.0,
14783 units: NumericSuffix::Mm,
14784 }),
14785 },
14786 end: Point2d {
14787 x: Expr::Var(Number {
14788 value: 0.0,
14789 units: NumericSuffix::Mm,
14790 }),
14791 y: Expr::Var(Number {
14792 value: 5.0,
14793 units: NumericSuffix::Mm,
14794 }),
14795 },
14796 center: Point2d {
14797 x: Expr::Var(Number {
14798 value: 0.0,
14799 units: NumericSuffix::Mm,
14800 }),
14801 y: Expr::Var(Number {
14802 value: 0.0,
14803 units: NumericSuffix::Mm,
14804 }),
14805 },
14806 construction: None,
14807 };
14808 let segment = SegmentCtor::Arc(arc_ctor);
14809 let (src_delta, scene_delta) = frontend
14810 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14811 .await
14812 .unwrap();
14813 assert!(
14814 src_delta
14815 .text
14816 .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
14817 "Expected arc in source, got: {}",
14818 src_delta.text
14819 );
14820 assert!(!scene_delta.new_objects.is_empty());
14821
14822 ctx.close().await;
14823 mock_ctx.close().await;
14824 }
14825
14826 #[tokio::test(flavor = "multi_thread")]
14827 async fn test_extra_newlines_add_circle() {
14828 let initial_source = "@settings(defaultLengthUnit = mm)
14830
14831
14832
14833sketch001 = sketch(on = XY) {
14834}
14835";
14836
14837 let program = Program::parse(initial_source).unwrap().0.unwrap();
14838 let mut frontend = FrontendState::new();
14839
14840 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14841 let mock_ctx = ExecutorContext::new_mock(None).await;
14842 let version = Version(0);
14843
14844 frontend.hack_set_program(&ctx, program).await.unwrap();
14845 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14846 let sketch_id = sketch_object.id;
14847
14848 let circle_ctor = CircleCtor {
14849 start: Point2d {
14850 x: Expr::Var(Number {
14851 value: 5.0,
14852 units: NumericSuffix::Mm,
14853 }),
14854 y: Expr::Var(Number {
14855 value: 0.0,
14856 units: NumericSuffix::Mm,
14857 }),
14858 },
14859 center: Point2d {
14860 x: Expr::Var(Number {
14861 value: 0.0,
14862 units: NumericSuffix::Mm,
14863 }),
14864 y: Expr::Var(Number {
14865 value: 0.0,
14866 units: NumericSuffix::Mm,
14867 }),
14868 },
14869 construction: None,
14870 };
14871 let segment = SegmentCtor::Circle(circle_ctor);
14872 let (src_delta, scene_delta) = frontend
14873 .add_segment(&mock_ctx, version, sketch_id, segment, None)
14874 .await
14875 .unwrap();
14876 assert!(
14877 src_delta
14878 .text
14879 .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
14880 "Expected circle in source, got: {}",
14881 src_delta.text
14882 );
14883 assert!(!scene_delta.new_objects.is_empty());
14884
14885 ctx.close().await;
14886 mock_ctx.close().await;
14887 }
14888
14889 #[tokio::test(flavor = "multi_thread")]
14890 async fn test_extra_newlines_add_constraint() {
14891 let initial_source = "@settings(defaultLengthUnit = mm)
14893
14894
14895
14896sketch001 = sketch(on = XY) {
14897 line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
14898 line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
14899}
14900";
14901
14902 let program = Program::parse(initial_source).unwrap().0.unwrap();
14903 let mut frontend = FrontendState::new();
14904
14905 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14906 let mock_ctx = ExecutorContext::new_mock(None).await;
14907 let version = Version(0);
14908 let project_id = ProjectId(0);
14909 let file_id = FileId(0);
14910
14911 frontend.hack_set_program(&ctx, program).await.unwrap();
14912 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14913 let sketch_id = sketch_object.id;
14914 let sketch = expect_sketch(sketch_object);
14915
14916 let line_ids: Vec<ObjectId> = sketch
14918 .segments
14919 .iter()
14920 .copied()
14921 .filter(|seg_id| {
14922 matches!(
14923 &frontend.scene_graph.objects[seg_id.0].kind,
14924 ObjectKind::Segment {
14925 segment: Segment::Line(_)
14926 }
14927 )
14928 })
14929 .collect();
14930 assert_eq!(line_ids.len(), 2, "Expected two line segments");
14931
14932 let line1 = &frontend.scene_graph.objects[line_ids[0].0];
14933 let ObjectKind::Segment {
14934 segment: Segment::Line(line1_data),
14935 } = &line1.kind
14936 else {
14937 panic!("Expected line");
14938 };
14939 let line2 = &frontend.scene_graph.objects[line_ids[1].0];
14940 let ObjectKind::Segment {
14941 segment: Segment::Line(line2_data),
14942 } = &line2.kind
14943 else {
14944 panic!("Expected line");
14945 };
14946
14947 let constraint = Constraint::Coincident(Coincident {
14949 segments: vec![line1_data.end.into(), line2_data.start.into()],
14950 });
14951
14952 frontend
14954 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14955 .await
14956 .unwrap();
14957 let (src_delta, _scene_delta) = frontend
14958 .add_constraint(&mock_ctx, version, sketch_id, constraint)
14959 .await
14960 .unwrap();
14961 assert!(
14962 src_delta.text.contains("coincident("),
14963 "Expected coincident constraint in source, got: {}",
14964 src_delta.text
14965 );
14966
14967 ctx.close().await;
14968 mock_ctx.close().await;
14969 }
14970
14971 #[tokio::test(flavor = "multi_thread")]
14972 async fn test_extra_newlines_add_line_then_edit_line() {
14973 let initial_source = "@settings(defaultLengthUnit = mm)
14975
14976
14977
14978sketch001 = sketch(on = XY) {
14979}
14980";
14981
14982 let program = Program::parse(initial_source).unwrap().0.unwrap();
14983 let mut frontend = FrontendState::new();
14984
14985 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
14986 let mock_ctx = ExecutorContext::new_mock(None).await;
14987 let version = Version(0);
14988
14989 frontend.hack_set_program(&ctx, program).await.unwrap();
14990 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14991 let sketch_id = sketch_object.id;
14992
14993 let line_ctor = LineCtor {
14995 start: Point2d {
14996 x: Expr::Number(Number {
14997 value: 0.0,
14998 units: NumericSuffix::Mm,
14999 }),
15000 y: Expr::Number(Number {
15001 value: 0.0,
15002 units: NumericSuffix::Mm,
15003 }),
15004 },
15005 end: Point2d {
15006 x: Expr::Number(Number {
15007 value: 10.0,
15008 units: NumericSuffix::Mm,
15009 }),
15010 y: Expr::Number(Number {
15011 value: 10.0,
15012 units: NumericSuffix::Mm,
15013 }),
15014 },
15015 construction: None,
15016 };
15017 let segment = SegmentCtor::Line(line_ctor);
15018 let (src_delta, scene_delta) = frontend
15019 .add_segment(&mock_ctx, version, sketch_id, segment, None)
15020 .await
15021 .unwrap();
15022 assert!(
15023 src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15024 "Expected line in source after add, got: {}",
15025 src_delta.text
15026 );
15027 let line_id = *scene_delta.new_objects.last().unwrap();
15029
15030 let line_ctor = LineCtor {
15032 start: Point2d {
15033 x: Expr::Number(Number {
15034 value: 1.0,
15035 units: NumericSuffix::Mm,
15036 }),
15037 y: Expr::Number(Number {
15038 value: 2.0,
15039 units: NumericSuffix::Mm,
15040 }),
15041 },
15042 end: Point2d {
15043 x: Expr::Number(Number {
15044 value: 13.0,
15045 units: NumericSuffix::Mm,
15046 }),
15047 y: Expr::Number(Number {
15048 value: 14.0,
15049 units: NumericSuffix::Mm,
15050 }),
15051 },
15052 construction: None,
15053 };
15054 let segments = vec![ExistingSegmentCtor {
15055 id: line_id,
15056 ctor: SegmentCtor::Line(line_ctor),
15057 }];
15058 let (src_delta, scene_delta) = frontend
15059 .edit_segments(&mock_ctx, version, sketch_id, segments)
15060 .await
15061 .unwrap();
15062 assert!(
15063 src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15064 "Expected edited line in source, got: {}",
15065 src_delta.text
15066 );
15067 assert_eq!(scene_delta.new_objects, vec![]);
15068
15069 ctx.close().await;
15070 mock_ctx.close().await;
15071 }
15072}