1use std::{
2 cell::Cell,
3 collections::{HashMap, HashSet},
4 ops::ControlFlow,
5};
6
7use indexmap::IndexMap;
8use kcl_error::SourceRange;
9use kittycad_modeling_cmds::units::UnitLength;
10
11use crate::{
12 ExecOutcome, ExecutorContext, Program,
13 collections::AhashIndexSet,
14 exec::WarningLevel,
15 execution::MockConfig,
16 fmt::format_number_literal,
17 front::{ArcCtor, Distance, Freedom, LinesEqualLength, Parallel, Perpendicular, PointCtor},
18 frontend::{
19 api::{
20 Error, Expr, FileId, Number, ObjectId, ObjectKind, ProjectId, SceneGraph, SceneGraphDelta, SourceDelta,
21 SourceRef, Version,
22 },
23 modify::{find_defined_names, next_free_name},
24 sketch::{
25 Coincident, Constraint, Diameter, ExistingSegmentCtor, Horizontal, LineCtor, Point2d, Radius, Segment,
26 SegmentCtor, SketchApi, SketchCtor, Vertical,
27 },
28 traverse::{MutateBodyItem, TraversalReturn, Visitor, dfs_mut},
29 },
30 parsing::ast::types as ast,
31 pretty::NumericSuffix,
32 std::constraints::LinesAtAngleKind,
33 walk::{NodeMut, Visitable},
34};
35
36pub(crate) mod api;
37pub(crate) mod modify;
38pub(crate) mod sketch;
39mod traverse;
40pub(crate) mod trim;
41
42struct ArcSizeConstraintParams {
43 points: Vec<ObjectId>,
44 function_name: &'static str,
45 value: f64,
46 units: NumericSuffix,
47 constraint_type_name: &'static str,
48}
49
50const POINT_FN: &str = "point";
51const POINT_AT_PARAM: &str = "at";
52const LINE_FN: &str = "line";
53const LINE_START_PARAM: &str = "start";
54const LINE_END_PARAM: &str = "end";
55const ARC_FN: &str = "arc";
56const ARC_START_PARAM: &str = "start";
57const ARC_END_PARAM: &str = "end";
58const ARC_CENTER_PARAM: &str = "center";
59
60const COINCIDENT_FN: &str = "coincident";
61const DIAMETER_FN: &str = "diameter";
62const DISTANCE_FN: &str = "distance";
63const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
64const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
65const EQUAL_LENGTH_FN: &str = "equalLength";
66const HORIZONTAL_FN: &str = "horizontal";
67const RADIUS_FN: &str = "radius";
68const VERTICAL_FN: &str = "vertical";
69
70const LINE_PROPERTY_START: &str = "start";
71const LINE_PROPERTY_END: &str = "end";
72
73const ARC_PROPERTY_START: &str = "start";
74const ARC_PROPERTY_END: &str = "end";
75const ARC_PROPERTY_CENTER: &str = "center";
76
77const CONSTRUCTION_PARAM: &str = "construction";
78
79#[derive(Debug, Clone, Copy)]
80enum EditDeleteKind {
81 Edit,
82 DeleteNonSketch,
83}
84
85impl EditDeleteKind {
86 fn is_delete(&self) -> bool {
88 match self {
89 EditDeleteKind::Edit => false,
90 EditDeleteKind::DeleteNonSketch => true,
91 }
92 }
93
94 fn to_change_kind(self) -> ChangeKind {
95 match self {
96 EditDeleteKind::Edit => ChangeKind::Edit,
97 EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
98 }
99 }
100}
101
102#[derive(Debug, Clone, Copy)]
103enum ChangeKind {
104 Add,
105 Edit,
106 Delete,
107 None,
108}
109
110#[derive(Debug, Clone)]
111pub struct FrontendState {
112 program: Program,
113 scene_graph: SceneGraph,
114 point_freedom_cache: HashMap<ObjectId, Freedom>,
117}
118
119impl Default for FrontendState {
120 fn default() -> Self {
121 Self::new()
122 }
123}
124
125impl FrontendState {
126 pub fn new() -> Self {
127 Self {
128 program: Program::empty(),
129 scene_graph: SceneGraph {
130 project: ProjectId(0),
131 file: FileId(0),
132 version: Version(0),
133 objects: Default::default(),
134 settings: Default::default(),
135 sketch_mode: Default::default(),
136 },
137 point_freedom_cache: HashMap::new(),
138 }
139 }
140
141 pub fn scene_graph(&self) -> &SceneGraph {
143 &self.scene_graph
144 }
145
146 pub fn default_length_unit(&self) -> UnitLength {
147 self.program
148 .meta_settings()
149 .ok()
150 .flatten()
151 .map(|settings| settings.default_length_units)
152 .unwrap_or(UnitLength::Millimeters)
153 }
154}
155
156impl SketchApi for FrontendState {
157 async fn execute_mock(
158 &mut self,
159 ctx: &ExecutorContext,
160 _version: Version,
161 sketch: ObjectId,
162 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
163 let mut truncated_program = self.program.clone();
164 self.only_sketch_block(sketch, ChangeKind::None, &mut truncated_program.ast)?;
165
166 let outcome = ctx
168 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
169 .await
170 .map_err(|err| Error {
171 msg: err.error.message().to_owned(),
172 })?;
173 let new_source = source_from_ast(&self.program.ast);
174 let src_delta = SourceDelta { text: new_source };
175 let outcome = self.update_state_after_exec(outcome, true);
177 let scene_graph_delta = SceneGraphDelta {
178 new_graph: self.scene_graph.clone(),
179 new_objects: Default::default(),
180 invalidates_ids: false,
181 exec_outcome: outcome,
182 };
183 Ok((src_delta, scene_graph_delta))
184 }
185
186 async fn new_sketch(
187 &mut self,
188 ctx: &ExecutorContext,
189 _project: ProjectId,
190 _file: FileId,
191 _version: Version,
192 args: SketchCtor,
193 ) -> api::Result<(SourceDelta, SceneGraphDelta, ObjectId)> {
194 let plane_ast = ast_name_expr(args.on);
198 let sketch_ast = ast::SketchBlock {
199 arguments: vec![ast::LabeledArg {
200 label: Some(ast::Identifier::new("on")),
201 arg: plane_ast,
202 }],
203 body: Default::default(),
204 is_being_edited: false,
205 non_code_meta: Default::default(),
206 digest: None,
207 };
208 let mut new_ast = self.program.ast.clone();
209 new_ast.set_experimental_features(Some(WarningLevel::Allow));
212 new_ast.body.push(ast::BodyItem::ExpressionStatement(ast::Node {
214 inner: ast::ExpressionStatement {
215 expression: ast::Expr::SketchBlock(Box::new(ast::Node {
216 inner: sketch_ast,
217 start: Default::default(),
218 end: Default::default(),
219 module_id: Default::default(),
220 outer_attrs: Default::default(),
221 pre_comments: Default::default(),
222 comment_start: Default::default(),
223 })),
224 digest: None,
225 },
226 start: Default::default(),
227 end: Default::default(),
228 module_id: Default::default(),
229 outer_attrs: Default::default(),
230 pre_comments: Default::default(),
231 comment_start: Default::default(),
232 }));
233 let new_source = source_from_ast(&new_ast);
235 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
237 if !errors.is_empty() {
238 return Err(Error {
239 msg: format!("Error parsing KCL source after adding sketch: {errors:?}"),
240 });
241 }
242 let Some(new_program) = new_program else {
243 return Err(Error {
244 msg: "No AST produced after adding sketch".to_owned(),
245 });
246 };
247
248 self.program = new_program.clone();
250
251 let outcome = ctx.run_with_caching(new_program.clone()).await.map_err(|err| Error {
254 msg: err.error.message().to_owned(),
255 })?;
256 let freedom_analysis_ran = true;
257
258 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
259
260 let Some(sketch_id) = self.scene_graph.objects.last().map(|object| object.id) else {
261 return Err(Error {
262 msg: "No objects in scene graph after adding sketch".to_owned(),
263 });
264 };
265 self.scene_graph.sketch_mode = Some(sketch_id);
267
268 let src_delta = SourceDelta { text: new_source };
269 let scene_graph_delta = SceneGraphDelta {
270 new_graph: self.scene_graph.clone(),
271 invalidates_ids: false,
272 new_objects: vec![sketch_id],
273 exec_outcome: outcome,
274 };
275 Ok((src_delta, scene_graph_delta, sketch_id))
276 }
277
278 async fn edit_sketch(
279 &mut self,
280 ctx: &ExecutorContext,
281 _project: ProjectId,
282 _file: FileId,
283 _version: Version,
284 sketch: ObjectId,
285 ) -> api::Result<SceneGraphDelta> {
286 let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| Error {
290 msg: format!("Sketch not found: {sketch:?}"),
291 })?;
292 let ObjectKind::Sketch(_) = &sketch_object.kind else {
293 return Err(Error {
294 msg: format!("Object is not a sketch: {sketch_object:?}"),
295 });
296 };
297
298 self.scene_graph.sketch_mode = Some(sketch);
300
301 let mut truncated_program = self.program.clone();
303 self.only_sketch_block(sketch, ChangeKind::None, &mut truncated_program.ast)?;
304
305 let outcome = ctx
308 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
309 .await
310 .map_err(|err| {
311 Error {
314 msg: err.error.message().to_owned(),
315 }
316 })?;
317
318 let outcome = self.update_state_after_exec(outcome, true);
320 let scene_graph_delta = SceneGraphDelta {
321 new_graph: self.scene_graph.clone(),
322 invalidates_ids: false,
323 new_objects: Vec::new(),
324 exec_outcome: outcome,
325 };
326 Ok(scene_graph_delta)
327 }
328
329 async fn exit_sketch(
330 &mut self,
331 ctx: &ExecutorContext,
332 _version: Version,
333 sketch: ObjectId,
334 ) -> api::Result<SceneGraph> {
335 #[cfg(not(target_arch = "wasm32"))]
337 let _ = sketch;
338 #[cfg(target_arch = "wasm32")]
339 if self.scene_graph.sketch_mode != Some(sketch) {
340 web_sys::console::warn_1(
341 &format!(
342 "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
343 &self.scene_graph.sketch_mode
344 )
345 .into(),
346 );
347 }
348 self.scene_graph.sketch_mode = None;
349
350 let outcome = ctx.run_with_caching(self.program.clone()).await.map_err(|err| {
352 Error {
355 msg: err.error.message().to_owned(),
356 }
357 })?;
358
359 self.update_state_after_exec(outcome, false);
361
362 Ok(self.scene_graph.clone())
363 }
364
365 async fn delete_sketch(
366 &mut self,
367 ctx: &ExecutorContext,
368 _version: Version,
369 sketch: ObjectId,
370 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
371 let mut new_ast = self.program.ast.clone();
374
375 let sketch_id = sketch;
377 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
378 msg: format!("Sketch not found: {sketch:?}"),
379 })?;
380 let ObjectKind::Sketch(_) = &sketch_object.kind else {
381 return Err(Error {
382 msg: format!("Object is not a sketch: {sketch_object:?}"),
383 });
384 };
385
386 self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)?;
388
389 self.execute_after_delete_sketch(ctx, &mut new_ast).await
390 }
391
392 async fn add_segment(
393 &mut self,
394 ctx: &ExecutorContext,
395 _version: Version,
396 sketch: ObjectId,
397 segment: SegmentCtor,
398 _label: Option<String>,
399 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
400 match segment {
402 SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
403 SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
404 SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
405 _ => Err(Error {
406 msg: format!("segment ctor not implemented yet: {segment:?}"),
407 }),
408 }
409 }
410
411 async fn edit_segments(
412 &mut self,
413 ctx: &ExecutorContext,
414 _version: Version,
415 sketch: ObjectId,
416 segments: Vec<ExistingSegmentCtor>,
417 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
418 let mut new_ast = self.program.ast.clone();
420 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
421
422 for segment in &segments {
425 segment_ids_edited.insert(segment.id);
426 }
427
428 let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
443
444 for segment in segments {
445 let segment_id = segment.id;
446 match segment.ctor {
447 SegmentCtor::Point(ctor) => {
448 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
450 && let ObjectKind::Segment { segment } = &segment_object.kind
451 && let Segment::Point(point) = segment
452 && let Some(owner_id) = point.owner
453 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
454 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
455 {
456 match owner_segment {
457 Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
458 if let Some(existing) = final_edits.get_mut(&owner_id) {
459 let SegmentCtor::Line(line_ctor) = existing else {
460 return Err(Error {
461 msg: format!("Internal: Expected line ctor for owner: {owner_object:?}"),
462 });
463 };
464 if line.start == segment_id {
466 line_ctor.start = ctor.position;
467 } else {
468 line_ctor.end = ctor.position;
469 }
470 } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
471 let mut line_ctor = line_ctor.clone();
473 if line.start == segment_id {
474 line_ctor.start = ctor.position;
475 } else {
476 line_ctor.end = ctor.position;
477 }
478 final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
479 } else {
480 return Err(Error {
482 msg: format!("Internal: Line does not have line ctor: {owner_object:?}"),
483 });
484 }
485 continue;
486 }
487 Segment::Arc(arc)
488 if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
489 {
490 if let Some(existing) = final_edits.get_mut(&owner_id) {
491 let SegmentCtor::Arc(arc_ctor) = existing else {
492 return Err(Error {
493 msg: format!("Internal: Expected arc ctor for owner: {owner_object:?}"),
494 });
495 };
496 if arc.start == segment_id {
497 arc_ctor.start = ctor.position;
498 } else if arc.end == segment_id {
499 arc_ctor.end = ctor.position;
500 } else {
501 arc_ctor.center = ctor.position;
502 }
503 } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
504 let mut arc_ctor = arc_ctor.clone();
505 if arc.start == segment_id {
506 arc_ctor.start = ctor.position;
507 } else if arc.end == segment_id {
508 arc_ctor.end = ctor.position;
509 } else {
510 arc_ctor.center = ctor.position;
511 }
512 final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
513 } else {
514 return Err(Error {
515 msg: format!("Internal: Arc does not have arc ctor: {owner_object:?}"),
516 });
517 }
518 continue;
519 }
520 _ => {}
521 }
522 }
523
524 final_edits.insert(segment_id, SegmentCtor::Point(ctor));
526 }
527 SegmentCtor::Line(ctor) => {
528 final_edits.insert(segment_id, SegmentCtor::Line(ctor));
529 }
530 SegmentCtor::Arc(ctor) => {
531 final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
532 }
533 other_ctor => {
534 final_edits.insert(segment_id, other_ctor);
535 }
536 }
537 }
538
539 for (segment_id, ctor) in final_edits {
540 match ctor {
541 SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment_id, ctor)?,
542 SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment_id, ctor)?,
543 SegmentCtor::Arc(ctor) => self.edit_arc(&mut new_ast, sketch, segment_id, ctor)?,
544 _ => {
545 return Err(Error {
546 msg: format!("segment ctor not implemented yet: {ctor:?}"),
547 });
548 }
549 }
550 }
551 self.execute_after_edit(ctx, sketch, segment_ids_edited, EditDeleteKind::Edit, &mut new_ast)
552 .await
553 }
554
555 async fn delete_objects(
556 &mut self,
557 ctx: &ExecutorContext,
558 _version: Version,
559 sketch: ObjectId,
560 constraint_ids: Vec<ObjectId>,
561 segment_ids: Vec<ObjectId>,
562 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
563 let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
567 let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
568
569 let mut delete_ids = AhashIndexSet::default();
572
573 for segment_id in segment_ids_set.iter().copied() {
574 if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
575 && let ObjectKind::Segment { segment } = &segment_object.kind
576 && let Segment::Point(point) = segment
577 && let Some(owner_id) = point.owner
578 && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
579 && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
580 && matches!(owner_segment, Segment::Line(_) | Segment::Arc(_))
581 {
582 delete_ids.insert(owner_id);
584 } else {
585 delete_ids.insert(segment_id);
587 }
588 }
589 self.add_dependent_constraints_to_delete(sketch, &delete_ids, &mut constraint_ids_set)?;
592
593 let mut new_ast = self.program.ast.clone();
594
595 for constraint_id in constraint_ids_set {
596 self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
597 }
598 for segment_id in delete_ids {
599 self.delete_segment(&mut new_ast, sketch, segment_id)?;
600 }
601
602 self.execute_after_edit(
603 ctx,
604 sketch,
605 Default::default(),
606 EditDeleteKind::DeleteNonSketch,
607 &mut new_ast,
608 )
609 .await
610 }
611
612 async fn add_constraint(
613 &mut self,
614 ctx: &ExecutorContext,
615 _version: Version,
616 sketch: ObjectId,
617 constraint: Constraint,
618 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
619 let original_program = self.program.clone();
623 let original_scene_graph = self.scene_graph.clone();
624
625 let mut new_ast = self.program.ast.clone();
626 let sketch_block_range = match constraint {
627 Constraint::Coincident(coincident) => self.add_coincident(sketch, coincident, &mut new_ast).await?,
628 Constraint::Distance(distance) => self.add_distance(sketch, distance, &mut new_ast).await?,
629 Constraint::HorizontalDistance(distance) => {
630 self.add_horizontal_distance(sketch, distance, &mut new_ast).await?
631 }
632 Constraint::VerticalDistance(distance) => {
633 self.add_vertical_distance(sketch, distance, &mut new_ast).await?
634 }
635 Constraint::Horizontal(horizontal) => self.add_horizontal(sketch, horizontal, &mut new_ast).await?,
636 Constraint::LinesEqualLength(lines_equal_length) => {
637 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
638 .await?
639 }
640 Constraint::Parallel(parallel) => self.add_parallel(sketch, parallel, &mut new_ast).await?,
641 Constraint::Perpendicular(perpendicular) => {
642 self.add_perpendicular(sketch, perpendicular, &mut new_ast).await?
643 }
644 Constraint::Radius(radius) => self.add_radius(sketch, radius, &mut new_ast).await?,
645 Constraint::Diameter(diameter) => self.add_diameter(sketch, diameter, &mut new_ast).await?,
646 Constraint::Vertical(vertical) => self.add_vertical(sketch, vertical, &mut new_ast).await?,
647 };
648
649 let result = self
650 .execute_after_add_constraint(ctx, sketch, sketch_block_range, &mut new_ast)
651 .await;
652
653 if result.is_err() {
655 self.program = original_program;
656 self.scene_graph = original_scene_graph;
657 }
658
659 result
660 }
661
662 async fn chain_segment(
663 &mut self,
664 ctx: &ExecutorContext,
665 version: Version,
666 sketch: ObjectId,
667 previous_segment_end_point_id: ObjectId,
668 segment: SegmentCtor,
669 _label: Option<String>,
670 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
671 let SegmentCtor::Line(line_ctor) = segment else {
675 return Err(Error {
676 msg: format!("chain_segment currently only supports Line segments, got: {segment:?}"),
677 });
678 };
679
680 let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
682
683 let new_line_id = first_scene_delta
686 .new_objects
687 .iter()
688 .find(|&obj_id| {
689 let obj = self.scene_graph.objects.get(obj_id.0);
690 if let Some(obj) = obj {
691 matches!(
692 &obj.kind,
693 ObjectKind::Segment {
694 segment: Segment::Line(_)
695 }
696 )
697 } else {
698 false
699 }
700 })
701 .ok_or_else(|| Error {
702 msg: "Failed to find new line segment in scene graph".to_string(),
703 })?;
704
705 let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| Error {
706 msg: format!("New line object not found: {new_line_id:?}"),
707 })?;
708
709 let ObjectKind::Segment {
710 segment: new_line_segment,
711 } = &new_line_obj.kind
712 else {
713 return Err(Error {
714 msg: format!("Object is not a segment: {new_line_obj:?}"),
715 });
716 };
717
718 let Segment::Line(new_line) = new_line_segment else {
719 return Err(Error {
720 msg: format!("Segment is not a line: {new_line_segment:?}"),
721 });
722 };
723
724 let new_line_start_point_id = new_line.start;
725
726 let coincident = Coincident {
728 segments: vec![previous_segment_end_point_id, new_line_start_point_id],
729 };
730
731 let (final_src_delta, final_scene_delta) = self
732 .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
733 .await?;
734
735 let mut combined_new_objects = first_scene_delta.new_objects.clone();
738 combined_new_objects.extend(final_scene_delta.new_objects);
739
740 let scene_graph_delta = SceneGraphDelta {
741 new_graph: self.scene_graph.clone(),
742 invalidates_ids: false,
743 new_objects: combined_new_objects,
744 exec_outcome: final_scene_delta.exec_outcome,
745 };
746
747 Ok((final_src_delta, scene_graph_delta))
748 }
749
750 async fn edit_constraint(
751 &mut self,
752 _ctx: &ExecutorContext,
753 _version: Version,
754 _sketch: ObjectId,
755 _constraint_id: ObjectId,
756 _constraint: Constraint,
757 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
758 todo!()
759 }
760
761 async fn batch_split_segment_operations(
769 &mut self,
770 ctx: &ExecutorContext,
771 _version: Version,
772 sketch: ObjectId,
773 edit_segments: Vec<ExistingSegmentCtor>,
774 add_constraints: Vec<Constraint>,
775 delete_constraint_ids: Vec<ObjectId>,
776 _new_segment_info: sketch::NewSegmentInfo,
777 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
778 let mut new_ast = self.program.ast.clone();
780 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
781
782 for segment in edit_segments {
784 segment_ids_edited.insert(segment.id);
785 match segment.ctor {
786 SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment.id, ctor)?,
787 SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment.id, ctor)?,
788 SegmentCtor::Arc(ctor) => self.edit_arc(&mut new_ast, sketch, segment.id, ctor)?,
789 _ => {
790 return Err(Error {
791 msg: format!("segment ctor not implemented yet: {segment:?}"),
792 });
793 }
794 }
795 }
796
797 for constraint in add_constraints {
799 match constraint {
800 Constraint::Coincident(coincident) => {
801 self.add_coincident(sketch, coincident, &mut new_ast).await?;
802 }
803 Constraint::Distance(distance) => {
804 self.add_distance(sketch, distance, &mut new_ast).await?;
805 }
806 Constraint::HorizontalDistance(distance) => {
807 self.add_horizontal_distance(sketch, distance, &mut new_ast).await?;
808 }
809 Constraint::VerticalDistance(distance) => {
810 self.add_vertical_distance(sketch, distance, &mut new_ast).await?;
811 }
812 Constraint::Horizontal(horizontal) => {
813 self.add_horizontal(sketch, horizontal, &mut new_ast).await?;
814 }
815 Constraint::LinesEqualLength(lines_equal_length) => {
816 self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
817 .await?;
818 }
819 Constraint::Parallel(parallel) => {
820 self.add_parallel(sketch, parallel, &mut new_ast).await?;
821 }
822 Constraint::Perpendicular(perpendicular) => {
823 self.add_perpendicular(sketch, perpendicular, &mut new_ast).await?;
824 }
825 Constraint::Vertical(vertical) => {
826 self.add_vertical(sketch, vertical, &mut new_ast).await?;
827 }
828 Constraint::Diameter(diameter) => {
829 self.add_diameter(sketch, diameter, &mut new_ast).await?;
830 }
831 Constraint::Radius(radius) => {
832 self.add_radius(sketch, radius, &mut new_ast).await?;
833 }
834 }
835 }
836
837 let mut constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
839 let segment_ids_set = AhashIndexSet::default();
840 self.add_dependent_constraints_to_delete(sketch, &segment_ids_set, &mut constraint_ids_set)?;
842
843 let has_constraint_deletions = !constraint_ids_set.is_empty();
844 for constraint_id in constraint_ids_set {
845 self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
846 }
847
848 let (source_delta, mut scene_graph_delta) = self
852 .execute_after_edit(ctx, sketch, segment_ids_edited, EditDeleteKind::Edit, &mut new_ast)
853 .await?;
854
855 if has_constraint_deletions {
858 scene_graph_delta.invalidates_ids = true;
859 }
860
861 Ok((source_delta, scene_graph_delta))
862 }
863
864 async fn batch_tail_cut_operations(
865 &mut self,
866 ctx: &ExecutorContext,
867 _version: Version,
868 sketch: ObjectId,
869 edit_segments: Vec<ExistingSegmentCtor>,
870 add_constraints: Vec<Constraint>,
871 delete_constraint_ids: Vec<ObjectId>,
872 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
873 let mut new_ast = self.program.ast.clone();
874 let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
875
876 for segment in edit_segments {
878 segment_ids_edited.insert(segment.id);
879 match segment.ctor {
880 SegmentCtor::Point(ctor) => self.edit_point(&mut new_ast, sketch, segment.id, ctor)?,
881 SegmentCtor::Line(ctor) => self.edit_line(&mut new_ast, sketch, segment.id, ctor)?,
882 SegmentCtor::Arc(ctor) => self.edit_arc(&mut new_ast, sketch, segment.id, ctor)?,
883 _ => {
884 return Err(Error {
885 msg: format!("segment ctor not implemented yet: {segment:?}"),
886 });
887 }
888 }
889 }
890
891 for constraint in add_constraints {
893 match constraint {
894 Constraint::Coincident(coincident) => {
895 self.add_coincident(sketch, coincident, &mut new_ast).await?;
896 }
897 other => {
898 return Err(Error {
899 msg: format!("unsupported constraint in tail cut batch: {other:?}"),
900 });
901 }
902 }
903 }
904
905 let mut constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
907 let segment_ids_set = AhashIndexSet::default();
908 self.add_dependent_constraints_to_delete(sketch, &segment_ids_set, &mut constraint_ids_set)?;
909
910 let has_constraint_deletions = !constraint_ids_set.is_empty();
911 for constraint_id in constraint_ids_set {
912 self.delete_constraint(&mut new_ast, sketch, constraint_id)?;
913 }
914
915 let (source_delta, mut scene_graph_delta) = self
919 .execute_after_edit(ctx, sketch, segment_ids_edited, EditDeleteKind::Edit, &mut new_ast)
920 .await?;
921
922 if has_constraint_deletions {
925 scene_graph_delta.invalidates_ids = true;
926 }
927
928 Ok((source_delta, scene_graph_delta))
929 }
930}
931
932impl FrontendState {
933 pub async fn hack_set_program(
934 &mut self,
935 ctx: &ExecutorContext,
936 program: Program,
937 ) -> api::Result<(SceneGraph, ExecOutcome)> {
938 self.program = program.clone();
939
940 self.point_freedom_cache.clear();
947 let outcome = ctx.run_with_caching(program).await.map_err(|err| Error {
948 msg: err.error.message().to_owned(),
949 })?;
950 let freedom_analysis_ran = true;
951
952 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
953
954 Ok((self.scene_graph.clone(), outcome))
955 }
956
957 async fn add_point(
958 &mut self,
959 ctx: &ExecutorContext,
960 sketch: ObjectId,
961 ctor: PointCtor,
962 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
963 let at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
965 let point_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
966 callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
967 unlabeled: None,
968 arguments: vec![ast::LabeledArg {
969 label: Some(ast::Identifier::new(POINT_AT_PARAM)),
970 arg: at_ast,
971 }],
972 digest: None,
973 non_code_meta: Default::default(),
974 })));
975
976 let sketch_id = sketch;
978 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
979 #[cfg(target_arch = "wasm32")]
980 web_sys::console::error_1(
981 &format!(
982 "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
983 &self.scene_graph.objects
984 )
985 .into(),
986 );
987 Error {
988 msg: format!("Sketch not found: {sketch:?}"),
989 }
990 })?;
991 let ObjectKind::Sketch(_) = &sketch_object.kind else {
992 return Err(Error {
993 msg: format!("Object is not a sketch: {sketch_object:?}"),
994 });
995 };
996 let mut new_ast = self.program.ast.clone();
998 let (sketch_block_range, _) = self.mutate_ast(
999 &mut new_ast,
1000 sketch_id,
1001 AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
1002 )?;
1003 let new_source = source_from_ast(&new_ast);
1005 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1007 if !errors.is_empty() {
1008 return Err(Error {
1009 msg: format!("Error parsing KCL source after adding point: {errors:?}"),
1010 });
1011 }
1012 let Some(new_program) = new_program else {
1013 return Err(Error {
1014 msg: "No AST produced after adding point".to_string(),
1015 });
1016 };
1017
1018 let point_source_range =
1019 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1020 msg: format!("Source range of point not found in sketch block: {sketch_block_range:?}; {err:?}"),
1021 })?;
1022 #[cfg(not(feature = "artifact-graph"))]
1023 let _ = point_source_range;
1024
1025 self.program = new_program.clone();
1027
1028 let mut truncated_program = new_program;
1030 self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
1031
1032 let outcome = ctx
1034 .run_mock(
1035 &truncated_program,
1036 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1037 )
1038 .await
1039 .map_err(|err| {
1040 Error {
1043 msg: err.error.message().to_owned(),
1044 }
1045 })?;
1046
1047 #[cfg(not(feature = "artifact-graph"))]
1048 let new_object_ids = Vec::new();
1049 #[cfg(feature = "artifact-graph")]
1050 let new_object_ids = {
1051 let segment_id = outcome
1052 .source_range_to_object
1053 .get(&point_source_range)
1054 .copied()
1055 .ok_or_else(|| Error {
1056 msg: format!("Source range of point not found: {point_source_range:?}"),
1057 })?;
1058 let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
1059 msg: format!("Segment not found: {segment_id:?}"),
1060 })?;
1061 let ObjectKind::Segment { segment } = &segment_object.kind else {
1062 return Err(Error {
1063 msg: format!("Object is not a segment: {segment_object:?}"),
1064 });
1065 };
1066 let Segment::Point(_) = segment else {
1067 return Err(Error {
1068 msg: format!("Segment is not a point: {segment:?}"),
1069 });
1070 };
1071 vec![segment_id]
1072 };
1073 let src_delta = SourceDelta { text: new_source };
1074 let outcome = self.update_state_after_exec(outcome, false);
1076 let scene_graph_delta = SceneGraphDelta {
1077 new_graph: self.scene_graph.clone(),
1078 invalidates_ids: false,
1079 new_objects: new_object_ids,
1080 exec_outcome: outcome,
1081 };
1082 Ok((src_delta, scene_graph_delta))
1083 }
1084
1085 async fn add_line(
1086 &mut self,
1087 ctx: &ExecutorContext,
1088 sketch: ObjectId,
1089 ctor: LineCtor,
1090 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1091 let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1093 let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1094 let mut arguments = vec![
1095 ast::LabeledArg {
1096 label: Some(ast::Identifier::new(LINE_START_PARAM)),
1097 arg: start_ast,
1098 },
1099 ast::LabeledArg {
1100 label: Some(ast::Identifier::new(LINE_END_PARAM)),
1101 arg: end_ast,
1102 },
1103 ];
1104 if ctor.construction == Some(true) {
1106 arguments.push(ast::LabeledArg {
1107 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1108 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1109 value: ast::LiteralValue::Bool(true),
1110 raw: "true".to_string(),
1111 digest: None,
1112 }))),
1113 });
1114 }
1115 let line_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1116 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
1117 unlabeled: None,
1118 arguments,
1119 digest: None,
1120 non_code_meta: Default::default(),
1121 })));
1122
1123 let sketch_id = sketch;
1125 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1126 msg: format!("Sketch not found: {sketch:?}"),
1127 })?;
1128 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1129 return Err(Error {
1130 msg: format!("Object is not a sketch: {sketch_object:?}"),
1131 });
1132 };
1133 let mut new_ast = self.program.ast.clone();
1135 let (sketch_block_range, _) = self.mutate_ast(
1136 &mut new_ast,
1137 sketch_id,
1138 AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
1139 )?;
1140 let new_source = source_from_ast(&new_ast);
1142 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1144 if !errors.is_empty() {
1145 return Err(Error {
1146 msg: format!("Error parsing KCL source after adding line: {errors:?}"),
1147 });
1148 }
1149 let Some(new_program) = new_program else {
1150 return Err(Error {
1151 msg: "No AST produced after adding line".to_string(),
1152 });
1153 };
1154 let line_source_range =
1155 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1156 msg: format!("Source range of line not found in sketch block: {sketch_block_range:?}; {err:?}"),
1157 })?;
1158 #[cfg(not(feature = "artifact-graph"))]
1159 let _ = line_source_range;
1160
1161 self.program = new_program.clone();
1163
1164 let mut truncated_program = new_program;
1166 self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
1167
1168 let outcome = ctx
1170 .run_mock(
1171 &truncated_program,
1172 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1173 )
1174 .await
1175 .map_err(|err| {
1176 Error {
1179 msg: err.error.message().to_owned(),
1180 }
1181 })?;
1182
1183 #[cfg(not(feature = "artifact-graph"))]
1184 let new_object_ids = Vec::new();
1185 #[cfg(feature = "artifact-graph")]
1186 let new_object_ids = {
1187 let segment_id = outcome
1188 .source_range_to_object
1189 .get(&line_source_range)
1190 .copied()
1191 .ok_or_else(|| Error {
1192 msg: format!("Source range of line not found: {line_source_range:?}"),
1193 })?;
1194 let segment_object = outcome.scene_object_by_id(segment_id).ok_or_else(|| Error {
1195 msg: format!("Segment not found: {segment_id:?}"),
1196 })?;
1197 let ObjectKind::Segment { segment } = &segment_object.kind else {
1198 return Err(Error {
1199 msg: format!("Object is not a segment: {segment_object:?}"),
1200 });
1201 };
1202 let Segment::Line(line) = segment else {
1203 return Err(Error {
1204 msg: format!("Segment is not a line: {segment:?}"),
1205 });
1206 };
1207 vec![line.start, line.end, segment_id]
1208 };
1209 let src_delta = SourceDelta { text: new_source };
1210 let outcome = self.update_state_after_exec(outcome, false);
1212 let scene_graph_delta = SceneGraphDelta {
1213 new_graph: self.scene_graph.clone(),
1214 invalidates_ids: false,
1215 new_objects: new_object_ids,
1216 exec_outcome: outcome,
1217 };
1218 Ok((src_delta, scene_graph_delta))
1219 }
1220
1221 async fn add_arc(
1222 &mut self,
1223 ctx: &ExecutorContext,
1224 sketch: ObjectId,
1225 ctor: ArcCtor,
1226 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1227 let start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1229 let end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1230 let center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
1231 let mut arguments = vec![
1232 ast::LabeledArg {
1233 label: Some(ast::Identifier::new(ARC_START_PARAM)),
1234 arg: start_ast,
1235 },
1236 ast::LabeledArg {
1237 label: Some(ast::Identifier::new(ARC_END_PARAM)),
1238 arg: end_ast,
1239 },
1240 ast::LabeledArg {
1241 label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
1242 arg: center_ast,
1243 },
1244 ];
1245 if ctor.construction == Some(true) {
1247 arguments.push(ast::LabeledArg {
1248 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
1249 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
1250 value: ast::LiteralValue::Bool(true),
1251 raw: "true".to_string(),
1252 digest: None,
1253 }))),
1254 });
1255 }
1256 let arc_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1257 callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
1258 unlabeled: None,
1259 arguments,
1260 digest: None,
1261 non_code_meta: Default::default(),
1262 })));
1263
1264 let sketch_id = sketch;
1266 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1267 msg: format!("Sketch not found: {sketch:?}"),
1268 })?;
1269 let ObjectKind::Sketch(_) = &sketch_object.kind else {
1270 return Err(Error {
1271 msg: format!("Object is not a sketch: {sketch_object:?}"),
1272 });
1273 };
1274 let mut new_ast = self.program.ast.clone();
1276 let (sketch_block_range, _) = self.mutate_ast(
1277 &mut new_ast,
1278 sketch_id,
1279 AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
1280 )?;
1281 let new_source = source_from_ast(&new_ast);
1283 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1285 if !errors.is_empty() {
1286 return Err(Error {
1287 msg: format!("Error parsing KCL source after adding arc: {errors:?}"),
1288 });
1289 }
1290 let Some(new_program) = new_program else {
1291 return Err(Error {
1292 msg: "No AST produced after adding arc".to_string(),
1293 });
1294 };
1295 let arc_source_range =
1296 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
1297 msg: format!("Source range of arc not found in sketch block: {sketch_block_range:?}; {err:?}"),
1298 })?;
1299 #[cfg(not(feature = "artifact-graph"))]
1300 let _ = arc_source_range;
1301
1302 self.program = new_program.clone();
1304
1305 let mut truncated_program = new_program;
1307 self.only_sketch_block(sketch, ChangeKind::Add, &mut truncated_program.ast)?;
1308
1309 let outcome = ctx
1311 .run_mock(
1312 &truncated_program,
1313 &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
1314 )
1315 .await
1316 .map_err(|err| {
1317 Error {
1320 msg: err.error.message().to_owned(),
1321 }
1322 })?;
1323
1324 #[cfg(not(feature = "artifact-graph"))]
1325 let new_object_ids = Vec::new();
1326 #[cfg(feature = "artifact-graph")]
1327 let new_object_ids = {
1328 let segment_id = outcome
1329 .source_range_to_object
1330 .get(&arc_source_range)
1331 .copied()
1332 .ok_or_else(|| Error {
1333 msg: format!("Source range of arc not found: {arc_source_range:?}"),
1334 })?;
1335 let segment_object = outcome.scene_objects.get(segment_id.0).ok_or_else(|| Error {
1336 msg: format!("Segment not found: {segment_id:?}"),
1337 })?;
1338 let ObjectKind::Segment { segment } = &segment_object.kind else {
1339 return Err(Error {
1340 msg: format!("Object is not a segment: {segment_object:?}"),
1341 });
1342 };
1343 let Segment::Arc(arc) = segment else {
1344 return Err(Error {
1345 msg: format!("Segment is not an arc: {segment:?}"),
1346 });
1347 };
1348 vec![arc.start, arc.end, arc.center, segment_id]
1349 };
1350 let src_delta = SourceDelta { text: new_source };
1351 let outcome = self.update_state_after_exec(outcome, false);
1353 let scene_graph_delta = SceneGraphDelta {
1354 new_graph: self.scene_graph.clone(),
1355 invalidates_ids: false,
1356 new_objects: new_object_ids,
1357 exec_outcome: outcome,
1358 };
1359 Ok((src_delta, scene_graph_delta))
1360 }
1361
1362 fn edit_point(
1363 &mut self,
1364 new_ast: &mut ast::Node<ast::Program>,
1365 sketch: ObjectId,
1366 point: ObjectId,
1367 ctor: PointCtor,
1368 ) -> api::Result<()> {
1369 let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| Error { msg: err.to_string() })?;
1371
1372 let sketch_id = sketch;
1374 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1375 msg: format!("Sketch not found: {sketch:?}"),
1376 })?;
1377 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1378 return Err(Error {
1379 msg: format!("Object is not a sketch: {sketch_object:?}"),
1380 });
1381 };
1382 sketch.segments.iter().find(|o| **o == point).ok_or_else(|| Error {
1383 msg: format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"),
1384 })?;
1385 let point_id = point;
1387 let point_object = self.scene_graph.objects.get(point_id.0).ok_or_else(|| Error {
1388 msg: format!("Point not found in scene graph: point={point:?}"),
1389 })?;
1390 let ObjectKind::Segment {
1391 segment: Segment::Point(point),
1392 } = &point_object.kind
1393 else {
1394 return Err(Error {
1395 msg: format!("Object is not a point segment: {point_object:?}"),
1396 });
1397 };
1398
1399 if let Some(owner_id) = point.owner {
1401 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| Error {
1402 msg: format!("Internal: Owner of point not found in scene graph: owner={owner_id:?}",),
1403 })?;
1404 let ObjectKind::Segment { segment } = &owner_object.kind else {
1405 return Err(Error {
1406 msg: format!("Internal: Owner of point is not a segment: {owner_object:?}"),
1407 });
1408 };
1409
1410 if let Segment::Line(line) = segment {
1412 let SegmentCtor::Line(line_ctor) = &line.ctor else {
1413 return Err(Error {
1414 msg: format!("Internal: Owner of point does not have line ctor: {owner_object:?}"),
1415 });
1416 };
1417 let mut line_ctor = line_ctor.clone();
1418 if line.start == point_id {
1420 line_ctor.start = ctor.position;
1421 } else if line.end == point_id {
1422 line_ctor.end = ctor.position;
1423 } else {
1424 return Err(Error {
1425 msg: format!(
1426 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
1427 ),
1428 });
1429 }
1430 return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
1431 }
1432
1433 if let Segment::Arc(arc) = segment {
1435 let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
1436 return Err(Error {
1437 msg: format!("Internal: Owner of point does not have arc ctor: {owner_object:?}"),
1438 });
1439 };
1440 let mut arc_ctor = arc_ctor.clone();
1441 if arc.center == point_id {
1443 arc_ctor.center = ctor.position;
1444 } else if arc.start == point_id {
1445 arc_ctor.start = ctor.position;
1446 } else if arc.end == point_id {
1447 arc_ctor.end = ctor.position;
1448 } else {
1449 return Err(Error {
1450 msg: format!(
1451 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
1452 ),
1453 });
1454 }
1455 return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
1456 }
1457
1458 }
1461
1462 self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
1464 Ok(())
1465 }
1466
1467 fn edit_line(
1468 &mut self,
1469 new_ast: &mut ast::Node<ast::Program>,
1470 sketch: ObjectId,
1471 line: ObjectId,
1472 ctor: LineCtor,
1473 ) -> api::Result<()> {
1474 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1476 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1477
1478 let sketch_id = sketch;
1480 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1481 msg: format!("Sketch not found: {sketch:?}"),
1482 })?;
1483 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1484 return Err(Error {
1485 msg: format!("Object is not a sketch: {sketch_object:?}"),
1486 });
1487 };
1488 sketch.segments.iter().find(|o| **o == line).ok_or_else(|| Error {
1489 msg: format!("Line not found in sketch: line={line:?}, sketch={sketch:?}"),
1490 })?;
1491 let line_id = line;
1493 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
1494 msg: format!("Line not found in scene graph: line={line:?}"),
1495 })?;
1496 let ObjectKind::Segment { .. } = &line_object.kind else {
1497 return Err(Error {
1498 msg: format!("Object is not a segment: {line_object:?}"),
1499 });
1500 };
1501
1502 self.mutate_ast(
1504 new_ast,
1505 line_id,
1506 AstMutateCommand::EditLine {
1507 start: new_start_ast,
1508 end: new_end_ast,
1509 construction: ctor.construction,
1510 },
1511 )?;
1512 Ok(())
1513 }
1514
1515 fn edit_arc(
1516 &mut self,
1517 new_ast: &mut ast::Node<ast::Program>,
1518 sketch: ObjectId,
1519 arc: ObjectId,
1520 ctor: ArcCtor,
1521 ) -> api::Result<()> {
1522 let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| Error { msg: err.to_string() })?;
1524 let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| Error { msg: err.to_string() })?;
1525 let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| Error { msg: err.to_string() })?;
1526
1527 let sketch_id = sketch;
1529 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1530 msg: format!("Sketch not found: {sketch:?}"),
1531 })?;
1532 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1533 return Err(Error {
1534 msg: format!("Object is not a sketch: {sketch_object:?}"),
1535 });
1536 };
1537 sketch.segments.iter().find(|o| **o == arc).ok_or_else(|| Error {
1538 msg: format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}"),
1539 })?;
1540 let arc_id = arc;
1542 let arc_object = self.scene_graph.objects.get(arc_id.0).ok_or_else(|| Error {
1543 msg: format!("Arc not found in scene graph: arc={arc:?}"),
1544 })?;
1545 let ObjectKind::Segment { .. } = &arc_object.kind else {
1546 return Err(Error {
1547 msg: format!("Object is not a segment: {arc_object:?}"),
1548 });
1549 };
1550
1551 self.mutate_ast(
1553 new_ast,
1554 arc_id,
1555 AstMutateCommand::EditArc {
1556 start: new_start_ast,
1557 end: new_end_ast,
1558 center: new_center_ast,
1559 construction: ctor.construction,
1560 },
1561 )?;
1562 Ok(())
1563 }
1564
1565 fn delete_segment(
1566 &mut self,
1567 new_ast: &mut ast::Node<ast::Program>,
1568 sketch: ObjectId,
1569 segment_id: ObjectId,
1570 ) -> api::Result<()> {
1571 let sketch_id = sketch;
1573 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1574 msg: format!("Sketch not found: {sketch:?}"),
1575 })?;
1576 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1577 return Err(Error {
1578 msg: format!("Object is not a sketch: {sketch_object:?}"),
1579 });
1580 };
1581 sketch
1582 .segments
1583 .iter()
1584 .find(|o| **o == segment_id)
1585 .ok_or_else(|| Error {
1586 msg: format!("Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"),
1587 })?;
1588 let segment_object = self.scene_graph.objects.get(segment_id.0).ok_or_else(|| Error {
1590 msg: format!("Segment not found in scene graph: segment={segment_id:?}"),
1591 })?;
1592 let ObjectKind::Segment { .. } = &segment_object.kind else {
1593 return Err(Error {
1594 msg: format!("Object is not a segment: {segment_object:?}"),
1595 });
1596 };
1597
1598 self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
1600 Ok(())
1601 }
1602
1603 fn delete_constraint(
1604 &mut self,
1605 new_ast: &mut ast::Node<ast::Program>,
1606 sketch: ObjectId,
1607 constraint_id: ObjectId,
1608 ) -> api::Result<()> {
1609 let sketch_id = sketch;
1611 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
1612 msg: format!("Sketch not found: {sketch:?}"),
1613 })?;
1614 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
1615 return Err(Error {
1616 msg: format!("Object is not a sketch: {sketch_object:?}"),
1617 });
1618 };
1619 sketch
1620 .constraints
1621 .iter()
1622 .find(|o| **o == constraint_id)
1623 .ok_or_else(|| Error {
1624 msg: format!("Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"),
1625 })?;
1626 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
1628 msg: format!("Constraint not found in scene graph: constraint={constraint_id:?}"),
1629 })?;
1630 let ObjectKind::Constraint { .. } = &constraint_object.kind else {
1631 return Err(Error {
1632 msg: format!("Object is not a constraint: {constraint_object:?}"),
1633 });
1634 };
1635
1636 self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
1638 Ok(())
1639 }
1640
1641 async fn execute_after_edit(
1642 &mut self,
1643 ctx: &ExecutorContext,
1644 sketch: ObjectId,
1645 segment_ids_edited: AhashIndexSet<ObjectId>,
1646 edit_kind: EditDeleteKind,
1647 new_ast: &mut ast::Node<ast::Program>,
1648 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1649 let new_source = source_from_ast(new_ast);
1651 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1653 if !errors.is_empty() {
1654 return Err(Error {
1655 msg: format!("Error parsing KCL source after editing: {errors:?}"),
1656 });
1657 }
1658 let Some(new_program) = new_program else {
1659 return Err(Error {
1660 msg: "No AST produced after editing".to_string(),
1661 });
1662 };
1663
1664 self.program = new_program.clone();
1666
1667 let is_delete = edit_kind.is_delete();
1669 let truncated_program = {
1670 let mut truncated_program = new_program;
1671 self.only_sketch_block(sketch, edit_kind.to_change_kind(), &mut truncated_program.ast)?;
1672 truncated_program
1673 };
1674
1675 #[cfg(not(feature = "artifact-graph"))]
1676 drop(segment_ids_edited);
1677
1678 let mock_config = MockConfig {
1680 sketch_block_id: Some(sketch),
1681 freedom_analysis: is_delete,
1682 #[cfg(feature = "artifact-graph")]
1683 segment_ids_edited: segment_ids_edited.clone(),
1684 ..Default::default()
1685 };
1686 let outcome = ctx.run_mock(&truncated_program, &mock_config).await.map_err(|err| {
1687 Error {
1690 msg: err.error.message().to_owned(),
1691 }
1692 })?;
1693
1694 let outcome = self.update_state_after_exec(outcome, is_delete);
1696
1697 #[cfg(feature = "artifact-graph")]
1698 let new_source = {
1699 let mut new_ast = self.program.ast.clone();
1704 for (var_range, value) in &outcome.var_solutions {
1705 let rounded = value.round(3);
1706 mutate_ast_node_by_source_range(
1707 &mut new_ast,
1708 *var_range,
1709 AstMutateCommand::EditVarInitialValue { value: rounded },
1710 )?;
1711 }
1712 source_from_ast(&new_ast)
1713 };
1714
1715 let src_delta = SourceDelta { text: new_source };
1716 let scene_graph_delta = SceneGraphDelta {
1717 new_graph: self.scene_graph.clone(),
1718 invalidates_ids: is_delete,
1719 new_objects: Vec::new(),
1720 exec_outcome: outcome,
1721 };
1722 Ok((src_delta, scene_graph_delta))
1723 }
1724
1725 async fn execute_after_delete_sketch(
1726 &mut self,
1727 ctx: &ExecutorContext,
1728 new_ast: &mut ast::Node<ast::Program>,
1729 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
1730 let new_source = source_from_ast(new_ast);
1732 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
1734 if !errors.is_empty() {
1735 return Err(Error {
1736 msg: format!("Error parsing KCL source after editing: {errors:?}"),
1737 });
1738 }
1739 let Some(new_program) = new_program else {
1740 return Err(Error {
1741 msg: "No AST produced after editing".to_string(),
1742 });
1743 };
1744
1745 self.program = new_program.clone();
1747
1748 let outcome = ctx.run_with_caching(new_program).await.map_err(|err| {
1754 Error {
1757 msg: err.error.message().to_owned(),
1758 }
1759 })?;
1760 let freedom_analysis_ran = true;
1761
1762 let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
1763
1764 let src_delta = SourceDelta { text: new_source };
1765 let scene_graph_delta = SceneGraphDelta {
1766 new_graph: self.scene_graph.clone(),
1767 invalidates_ids: true,
1768 new_objects: Vec::new(),
1769 exec_outcome: outcome,
1770 };
1771 Ok((src_delta, scene_graph_delta))
1772 }
1773
1774 fn point_id_to_ast_reference(
1779 &self,
1780 point_id: ObjectId,
1781 new_ast: &mut ast::Node<ast::Program>,
1782 ) -> api::Result<ast::Expr> {
1783 let point_object = self.scene_graph.objects.get(point_id.0).ok_or_else(|| Error {
1784 msg: format!("Point not found: {point_id:?}"),
1785 })?;
1786 let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
1787 return Err(Error {
1788 msg: format!("Object is not a segment: {point_object:?}"),
1789 });
1790 };
1791 let Segment::Point(point) = point_segment else {
1792 return Err(Error {
1793 msg: format!("Only points are currently supported: {point_object:?}"),
1794 });
1795 };
1796
1797 if let Some(owner_id) = point.owner {
1798 let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| Error {
1799 msg: format!("Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"),
1800 })?;
1801 let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
1802 return Err(Error {
1803 msg: format!("Owner of point is not a segment: {owner_object:?}"),
1804 });
1805 };
1806
1807 match owner_segment {
1808 Segment::Line(line) => {
1809 let property = if line.start == point_id {
1810 LINE_PROPERTY_START
1811 } else if line.end == point_id {
1812 LINE_PROPERTY_END
1813 } else {
1814 return Err(Error {
1815 msg: format!(
1816 "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
1817 ),
1818 });
1819 };
1820 get_or_insert_ast_reference(new_ast, &owner_object.source, "line", Some(property))
1821 }
1822 Segment::Arc(arc) => {
1823 let property = if arc.start == point_id {
1824 ARC_PROPERTY_START
1825 } else if arc.end == point_id {
1826 ARC_PROPERTY_END
1827 } else if arc.center == point_id {
1828 ARC_PROPERTY_CENTER
1829 } else {
1830 return Err(Error {
1831 msg: format!(
1832 "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
1833 ),
1834 });
1835 };
1836 get_or_insert_ast_reference(new_ast, &owner_object.source, "arc", Some(property))
1837 }
1838 _ => Err(Error {
1839 msg: format!(
1840 "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
1841 ),
1842 }),
1843 }
1844 } else {
1845 get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
1847 }
1848 }
1849
1850 async fn add_coincident(
1851 &mut self,
1852 sketch: ObjectId,
1853 coincident: Coincident,
1854 new_ast: &mut ast::Node<ast::Program>,
1855 ) -> api::Result<SourceRange> {
1856 let &[seg0_id, seg1_id] = coincident.segments.as_slice() else {
1857 return Err(Error {
1858 msg: format!(
1859 "Coincident constraint must have exactly 2 segments, got {}",
1860 coincident.segments.len()
1861 ),
1862 });
1863 };
1864 let sketch_id = sketch;
1865
1866 let seg0_object = self.scene_graph.objects.get(seg0_id.0).ok_or_else(|| Error {
1868 msg: format!("Object not found: {seg0_id:?}"),
1869 })?;
1870 let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
1871 return Err(Error {
1872 msg: format!("Object is not a segment: {seg0_object:?}"),
1873 });
1874 };
1875 let seg0_ast = match seg0_segment {
1876 Segment::Point(_) => {
1877 self.point_id_to_ast_reference(seg0_id, new_ast)?
1879 }
1880 Segment::Line(_) => {
1881 get_or_insert_ast_reference(new_ast, &seg0_object.source, "line", None)?
1883 }
1884 Segment::Arc(_) | Segment::Circle(_) => {
1885 get_or_insert_ast_reference(new_ast, &seg0_object.source, "arc", None)?
1887 }
1888 };
1889
1890 let seg1_object = self.scene_graph.objects.get(seg1_id.0).ok_or_else(|| Error {
1892 msg: format!("Object not found: {seg1_id:?}"),
1893 })?;
1894 let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
1895 return Err(Error {
1896 msg: format!("Object is not a segment: {seg1_object:?}"),
1897 });
1898 };
1899 let seg1_ast = match seg1_segment {
1900 Segment::Point(_) => {
1901 self.point_id_to_ast_reference(seg1_id, new_ast)?
1903 }
1904 Segment::Line(_) => {
1905 get_or_insert_ast_reference(new_ast, &seg1_object.source, "line", None)?
1907 }
1908 Segment::Arc(_) | Segment::Circle(_) => {
1909 get_or_insert_ast_reference(new_ast, &seg1_object.source, "arc", None)?
1911 }
1912 };
1913
1914 let coincident_ast = create_coincident_ast(seg0_ast, seg1_ast);
1916
1917 let (sketch_block_range, _) = self.mutate_ast(
1919 new_ast,
1920 sketch_id,
1921 AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
1922 )?;
1923 Ok(sketch_block_range)
1924 }
1925
1926 async fn add_distance(
1927 &mut self,
1928 sketch: ObjectId,
1929 distance: Distance,
1930 new_ast: &mut ast::Node<ast::Program>,
1931 ) -> api::Result<SourceRange> {
1932 let &[pt0_id, pt1_id] = distance.points.as_slice() else {
1933 return Err(Error {
1934 msg: format!(
1935 "Distance constraint must have exactly 2 points, got {}",
1936 distance.points.len()
1937 ),
1938 });
1939 };
1940 let sketch_id = sketch;
1941
1942 let pt0_ast = self.point_id_to_ast_reference(pt0_id, new_ast)?;
1944 let pt1_ast = self.point_id_to_ast_reference(pt1_id, new_ast)?;
1945
1946 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
1948 callee: ast::Node::no_src(ast_sketch2_name(DISTANCE_FN)),
1949 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
1950 ast::ArrayExpression {
1951 elements: vec![pt0_ast, pt1_ast],
1952 digest: None,
1953 non_code_meta: Default::default(),
1954 },
1955 )))),
1956 arguments: Default::default(),
1957 digest: None,
1958 non_code_meta: Default::default(),
1959 })));
1960 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
1961 left: distance_call_ast,
1962 operator: ast::BinaryOperator::Eq,
1963 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
1964 value: ast::LiteralValue::Number {
1965 value: distance.distance.value,
1966 suffix: distance.distance.units,
1967 },
1968 raw: format_number_literal(distance.distance.value, distance.distance.units).map_err(|_| Error {
1969 msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
1970 })?,
1971 digest: None,
1972 }))),
1973 digest: None,
1974 })));
1975
1976 let (sketch_block_range, _) = self.mutate_ast(
1978 new_ast,
1979 sketch_id,
1980 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
1981 )?;
1982 Ok(sketch_block_range)
1983 }
1984
1985 async fn add_radius(
1986 &mut self,
1987 sketch: ObjectId,
1988 radius: Radius,
1989 new_ast: &mut ast::Node<ast::Program>,
1990 ) -> api::Result<SourceRange> {
1991 let params = ArcSizeConstraintParams {
1992 points: vec![radius.arc],
1993 function_name: RADIUS_FN,
1994 value: radius.radius.value,
1995 units: radius.radius.units,
1996 constraint_type_name: "Radius",
1997 };
1998 self.add_arc_size_constraint(sketch, params, new_ast).await
1999 }
2000
2001 async fn add_diameter(
2002 &mut self,
2003 sketch: ObjectId,
2004 diameter: Diameter,
2005 new_ast: &mut ast::Node<ast::Program>,
2006 ) -> api::Result<SourceRange> {
2007 let params = ArcSizeConstraintParams {
2008 points: vec![diameter.arc],
2009 function_name: DIAMETER_FN,
2010 value: diameter.diameter.value,
2011 units: diameter.diameter.units,
2012 constraint_type_name: "Diameter",
2013 };
2014 self.add_arc_size_constraint(sketch, params, new_ast).await
2015 }
2016
2017 async fn add_arc_size_constraint(
2018 &mut self,
2019 sketch: ObjectId,
2020 params: ArcSizeConstraintParams,
2021 new_ast: &mut ast::Node<ast::Program>,
2022 ) -> api::Result<SourceRange> {
2023 let sketch_id = sketch;
2024
2025 if params.points.len() != 1 {
2027 return Err(Error {
2028 msg: format!(
2029 "{} constraint must have exactly 1 argument (an arc segment), got {}",
2030 params.constraint_type_name,
2031 params.points.len()
2032 ),
2033 });
2034 }
2035
2036 let arc_id = params.points[0];
2037 let arc_object = self.scene_graph.objects.get(arc_id.0).ok_or_else(|| Error {
2038 msg: format!("Arc segment not found: {arc_id:?}"),
2039 })?;
2040 let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
2041 return Err(Error {
2042 msg: format!("Object is not a segment: {arc_object:?}"),
2043 });
2044 };
2045 let Segment::Arc(_) = arc_segment else {
2046 return Err(Error {
2047 msg: format!(
2048 "{} constraint argument must be an arc segment, got: {arc_segment:?}",
2049 params.constraint_type_name
2050 ),
2051 });
2052 };
2053 let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, "arc", None)?;
2055
2056 let call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2058 callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
2059 unlabeled: Some(arc_ast),
2060 arguments: Default::default(),
2061 digest: None,
2062 non_code_meta: Default::default(),
2063 })));
2064 let constraint_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2065 left: call_ast,
2066 operator: ast::BinaryOperator::Eq,
2067 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2068 value: ast::LiteralValue::Number {
2069 value: params.value,
2070 suffix: params.units,
2071 },
2072 raw: format_number_literal(params.value, params.units).map_err(|_| Error {
2073 msg: format!("Could not format numeric suffix: {:?}", params.units),
2074 })?,
2075 digest: None,
2076 }))),
2077 digest: None,
2078 })));
2079
2080 let (sketch_block_range, _) = self.mutate_ast(
2082 new_ast,
2083 sketch_id,
2084 AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
2085 )?;
2086 Ok(sketch_block_range)
2087 }
2088
2089 async fn add_horizontal_distance(
2090 &mut self,
2091 sketch: ObjectId,
2092 distance: Distance,
2093 new_ast: &mut ast::Node<ast::Program>,
2094 ) -> api::Result<SourceRange> {
2095 let &[pt0_id, pt1_id] = distance.points.as_slice() else {
2096 return Err(Error {
2097 msg: format!(
2098 "Horizontal distance constraint must have exactly 2 points, got {}",
2099 distance.points.len()
2100 ),
2101 });
2102 };
2103 let sketch_id = sketch;
2104
2105 let pt0_ast = self.point_id_to_ast_reference(pt0_id, new_ast)?;
2107 let pt1_ast = self.point_id_to_ast_reference(pt1_id, new_ast)?;
2108
2109 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2111 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_DISTANCE_FN)),
2112 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2113 ast::ArrayExpression {
2114 elements: vec![pt0_ast, pt1_ast],
2115 digest: None,
2116 non_code_meta: Default::default(),
2117 },
2118 )))),
2119 arguments: Default::default(),
2120 digest: None,
2121 non_code_meta: Default::default(),
2122 })));
2123 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2124 left: distance_call_ast,
2125 operator: ast::BinaryOperator::Eq,
2126 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2127 value: ast::LiteralValue::Number {
2128 value: distance.distance.value,
2129 suffix: distance.distance.units,
2130 },
2131 raw: format_number_literal(distance.distance.value, distance.distance.units).map_err(|_| Error {
2132 msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
2133 })?,
2134 digest: None,
2135 }))),
2136 digest: None,
2137 })));
2138
2139 let (sketch_block_range, _) = self.mutate_ast(
2141 new_ast,
2142 sketch_id,
2143 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
2144 )?;
2145 Ok(sketch_block_range)
2146 }
2147
2148 async fn add_vertical_distance(
2149 &mut self,
2150 sketch: ObjectId,
2151 distance: Distance,
2152 new_ast: &mut ast::Node<ast::Program>,
2153 ) -> api::Result<SourceRange> {
2154 let &[pt0_id, pt1_id] = distance.points.as_slice() else {
2155 return Err(Error {
2156 msg: format!(
2157 "Vertical distance constraint must have exactly 2 points, got {}",
2158 distance.points.len()
2159 ),
2160 });
2161 };
2162 let sketch_id = sketch;
2163
2164 let pt0_ast = self.point_id_to_ast_reference(pt0_id, new_ast)?;
2166 let pt1_ast = self.point_id_to_ast_reference(pt1_id, new_ast)?;
2167
2168 let distance_call_ast = ast::BinaryPart::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2170 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_DISTANCE_FN)),
2171 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2172 ast::ArrayExpression {
2173 elements: vec![pt0_ast, pt1_ast],
2174 digest: None,
2175 non_code_meta: Default::default(),
2176 },
2177 )))),
2178 arguments: Default::default(),
2179 digest: None,
2180 non_code_meta: Default::default(),
2181 })));
2182 let distance_ast = ast::Expr::BinaryExpression(Box::new(ast::Node::no_src(ast::BinaryExpression {
2183 left: distance_call_ast,
2184 operator: ast::BinaryOperator::Eq,
2185 right: ast::BinaryPart::Literal(Box::new(ast::Node::no_src(ast::Literal {
2186 value: ast::LiteralValue::Number {
2187 value: distance.distance.value,
2188 suffix: distance.distance.units,
2189 },
2190 raw: format_number_literal(distance.distance.value, distance.distance.units).map_err(|_| Error {
2191 msg: format!("Could not format numeric suffix: {:?}", distance.distance.units),
2192 })?,
2193 digest: None,
2194 }))),
2195 digest: None,
2196 })));
2197
2198 let (sketch_block_range, _) = self.mutate_ast(
2200 new_ast,
2201 sketch_id,
2202 AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
2203 )?;
2204 Ok(sketch_block_range)
2205 }
2206
2207 async fn add_horizontal(
2208 &mut self,
2209 sketch: ObjectId,
2210 horizontal: Horizontal,
2211 new_ast: &mut ast::Node<ast::Program>,
2212 ) -> api::Result<SourceRange> {
2213 let sketch_id = sketch;
2214
2215 let line_id = horizontal.line;
2217 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
2218 msg: format!("Line not found: {line_id:?}"),
2219 })?;
2220 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2221 return Err(Error {
2222 msg: format!("Object is not a segment: {line_object:?}"),
2223 });
2224 };
2225 let Segment::Line(_) = line_segment else {
2226 return Err(Error {
2227 msg: format!("Only lines can be made horizontal: {line_object:?}"),
2228 });
2229 };
2230 let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
2231
2232 let horizontal_ast = create_horizontal_ast(line_ast);
2234
2235 let (sketch_block_range, _) = self.mutate_ast(
2237 new_ast,
2238 sketch_id,
2239 AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
2240 )?;
2241 Ok(sketch_block_range)
2242 }
2243
2244 async fn add_lines_equal_length(
2245 &mut self,
2246 sketch: ObjectId,
2247 lines_equal_length: LinesEqualLength,
2248 new_ast: &mut ast::Node<ast::Program>,
2249 ) -> api::Result<SourceRange> {
2250 let &[line0_id, line1_id] = lines_equal_length.lines.as_slice() else {
2251 return Err(Error {
2252 msg: format!(
2253 "Lines equal length constraint must have exactly 2 lines, got {}",
2254 lines_equal_length.lines.len()
2255 ),
2256 });
2257 };
2258
2259 let sketch_id = sketch;
2260
2261 let line0_object = self.scene_graph.objects.get(line0_id.0).ok_or_else(|| Error {
2263 msg: format!("Line not found: {line0_id:?}"),
2264 })?;
2265 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
2266 return Err(Error {
2267 msg: format!("Object is not a segment: {line0_object:?}"),
2268 });
2269 };
2270 let Segment::Line(_) = line0_segment else {
2271 return Err(Error {
2272 msg: format!("Only lines can be made equal length: {line0_object:?}"),
2273 });
2274 };
2275 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
2276
2277 let line1_object = self.scene_graph.objects.get(line1_id.0).ok_or_else(|| Error {
2278 msg: format!("Line not found: {line1_id:?}"),
2279 })?;
2280 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
2281 return Err(Error {
2282 msg: format!("Object is not a segment: {line1_object:?}"),
2283 });
2284 };
2285 let Segment::Line(_) = line1_segment else {
2286 return Err(Error {
2287 msg: format!("Only lines can be made equal length: {line1_object:?}"),
2288 });
2289 };
2290 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
2291
2292 let equal_length_ast = create_equal_length_ast(line0_ast, line1_ast);
2294
2295 let (sketch_block_range, _) = self.mutate_ast(
2297 new_ast,
2298 sketch_id,
2299 AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
2300 )?;
2301 Ok(sketch_block_range)
2302 }
2303
2304 async fn add_parallel(
2305 &mut self,
2306 sketch: ObjectId,
2307 parallel: Parallel,
2308 new_ast: &mut ast::Node<ast::Program>,
2309 ) -> api::Result<SourceRange> {
2310 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Parallel, parallel.lines, new_ast)
2311 .await
2312 }
2313
2314 async fn add_perpendicular(
2315 &mut self,
2316 sketch: ObjectId,
2317 perpendicular: Perpendicular,
2318 new_ast: &mut ast::Node<ast::Program>,
2319 ) -> api::Result<SourceRange> {
2320 self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
2321 .await
2322 }
2323
2324 async fn add_lines_at_angle_constraint(
2325 &mut self,
2326 sketch: ObjectId,
2327 angle_kind: LinesAtAngleKind,
2328 lines: Vec<ObjectId>,
2329 new_ast: &mut ast::Node<ast::Program>,
2330 ) -> api::Result<SourceRange> {
2331 let &[line0_id, line1_id] = lines.as_slice() else {
2332 return Err(Error {
2333 msg: format!(
2334 "{} constraint must have exactly 2 lines, got {}",
2335 angle_kind.to_function_name(),
2336 lines.len()
2337 ),
2338 });
2339 };
2340
2341 let sketch_id = sketch;
2342
2343 let line0_object = self.scene_graph.objects.get(line0_id.0).ok_or_else(|| Error {
2345 msg: format!("Line not found: {line0_id:?}"),
2346 })?;
2347 let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
2348 return Err(Error {
2349 msg: format!("Object is not a segment: {line0_object:?}"),
2350 });
2351 };
2352 let Segment::Line(_) = line0_segment else {
2353 return Err(Error {
2354 msg: format!(
2355 "Only lines can be made {}: {line0_object:?}",
2356 angle_kind.to_function_name()
2357 ),
2358 });
2359 };
2360 let line0_ast = get_or_insert_ast_reference(new_ast, &line0_object.source.clone(), "line", None)?;
2361
2362 let line1_object = self.scene_graph.objects.get(line1_id.0).ok_or_else(|| Error {
2363 msg: format!("Line not found: {line1_id:?}"),
2364 })?;
2365 let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
2366 return Err(Error {
2367 msg: format!("Object is not a segment: {line1_object:?}"),
2368 });
2369 };
2370 let Segment::Line(_) = line1_segment else {
2371 return Err(Error {
2372 msg: format!(
2373 "Only lines can be made {}: {line1_object:?}",
2374 angle_kind.to_function_name()
2375 ),
2376 });
2377 };
2378 let line1_ast = get_or_insert_ast_reference(new_ast, &line1_object.source.clone(), "line", None)?;
2379
2380 let call_ast = ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
2382 callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
2383 unlabeled: Some(ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(
2384 ast::ArrayExpression {
2385 elements: vec![line0_ast, line1_ast],
2386 digest: None,
2387 non_code_meta: Default::default(),
2388 },
2389 )))),
2390 arguments: Default::default(),
2391 digest: None,
2392 non_code_meta: Default::default(),
2393 })));
2394
2395 let (sketch_block_range, _) = self.mutate_ast(
2397 new_ast,
2398 sketch_id,
2399 AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
2400 )?;
2401 Ok(sketch_block_range)
2402 }
2403
2404 async fn add_vertical(
2405 &mut self,
2406 sketch: ObjectId,
2407 vertical: Vertical,
2408 new_ast: &mut ast::Node<ast::Program>,
2409 ) -> api::Result<SourceRange> {
2410 let sketch_id = sketch;
2411
2412 let line_id = vertical.line;
2414 let line_object = self.scene_graph.objects.get(line_id.0).ok_or_else(|| Error {
2415 msg: format!("Line not found: {line_id:?}"),
2416 })?;
2417 let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
2418 return Err(Error {
2419 msg: format!("Object is not a segment: {line_object:?}"),
2420 });
2421 };
2422 let Segment::Line(_) = line_segment else {
2423 return Err(Error {
2424 msg: format!("Only lines can be made vertical: {line_object:?}"),
2425 });
2426 };
2427 let line_ast = get_or_insert_ast_reference(new_ast, &line_object.source.clone(), "line", None)?;
2428
2429 let vertical_ast = create_vertical_ast(line_ast);
2431
2432 let (sketch_block_range, _) = self.mutate_ast(
2434 new_ast,
2435 sketch_id,
2436 AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
2437 )?;
2438 Ok(sketch_block_range)
2439 }
2440
2441 async fn execute_after_add_constraint(
2442 &mut self,
2443 ctx: &ExecutorContext,
2444 sketch_id: ObjectId,
2445 #[cfg_attr(not(feature = "artifact-graph"), allow(unused_variables))] sketch_block_range: SourceRange,
2446 new_ast: &mut ast::Node<ast::Program>,
2447 ) -> api::Result<(SourceDelta, SceneGraphDelta)> {
2448 let new_source = source_from_ast(new_ast);
2450 let (new_program, errors) = Program::parse(&new_source).map_err(|err| Error { msg: err.to_string() })?;
2452 if !errors.is_empty() {
2453 return Err(Error {
2454 msg: format!("Error parsing KCL source after adding constraint: {errors:?}"),
2455 });
2456 }
2457 let Some(new_program) = new_program else {
2458 return Err(Error {
2459 msg: "No AST produced after adding constraint".to_string(),
2460 });
2461 };
2462 #[cfg(feature = "artifact-graph")]
2463 let constraint_source_range =
2464 find_sketch_block_added_item(&new_program.ast, sketch_block_range).map_err(|err| Error {
2465 msg: format!(
2466 "Source range of new constraint not found in sketch block: {sketch_block_range:?}; {err:?}"
2467 ),
2468 })?;
2469
2470 let mut truncated_program = new_program.clone();
2473 self.only_sketch_block(sketch_id, ChangeKind::Add, &mut truncated_program.ast)?;
2474
2475 let outcome = ctx
2477 .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
2478 .await
2479 .map_err(|err| {
2480 Error {
2483 msg: err.error.message().to_owned(),
2484 }
2485 })?;
2486
2487 #[cfg(not(feature = "artifact-graph"))]
2488 let new_object_ids = Vec::new();
2489 #[cfg(feature = "artifact-graph")]
2490 let new_object_ids = {
2491 let constraint_id = outcome
2493 .source_range_to_object
2494 .get(&constraint_source_range)
2495 .copied()
2496 .ok_or_else(|| Error {
2497 msg: format!("Source range of constraint not found: {constraint_source_range:?}"),
2498 })?;
2499 vec![constraint_id]
2500 };
2501
2502 self.program = new_program;
2505
2506 let outcome = self.update_state_after_exec(outcome, true);
2508
2509 let src_delta = SourceDelta { text: new_source };
2510 let scene_graph_delta = SceneGraphDelta {
2511 new_graph: self.scene_graph.clone(),
2512 invalidates_ids: false,
2513 new_objects: new_object_ids,
2514 exec_outcome: outcome,
2515 };
2516 Ok((src_delta, scene_graph_delta))
2517 }
2518
2519 fn add_dependent_constraints_to_delete(
2522 &self,
2523 sketch_id: ObjectId,
2524 segment_ids_set: &AhashIndexSet<ObjectId>,
2525 constraint_ids_set: &mut AhashIndexSet<ObjectId>,
2526 ) -> api::Result<()> {
2527 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
2529 msg: format!("Sketch not found: {sketch_id:?}"),
2530 })?;
2531 let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2532 return Err(Error {
2533 msg: format!("Object is not a sketch: {sketch_object:?}"),
2534 });
2535 };
2536 for constraint_id in &sketch.constraints {
2537 let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| Error {
2538 msg: format!("Constraint not found: {constraint_id:?}"),
2539 })?;
2540 let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
2541 return Err(Error {
2542 msg: format!("Object is not a constraint: {constraint_object:?}"),
2543 });
2544 };
2545 let depends_on_segment = match constraint {
2546 Constraint::Coincident(c) => c.segments.iter().any(|seg_id| {
2547 if segment_ids_set.contains(seg_id) {
2549 return true;
2550 }
2551 let seg_object = self.scene_graph.objects.get(seg_id.0);
2553 if let Some(obj) = seg_object
2554 && let ObjectKind::Segment { segment } = &obj.kind
2555 && let Segment::Point(pt) = segment
2556 && let Some(owner_line_id) = pt.owner
2557 {
2558 return segment_ids_set.contains(&owner_line_id);
2559 }
2560 false
2561 }),
2562 Constraint::Distance(d) => d.points.iter().any(|pt_id| {
2563 if segment_ids_set.contains(pt_id) {
2564 return true;
2565 }
2566 let pt_object = self.scene_graph.objects.get(pt_id.0);
2567 if let Some(obj) = pt_object
2568 && let ObjectKind::Segment { segment } = &obj.kind
2569 && let Segment::Point(pt) = segment
2570 && let Some(owner_line_id) = pt.owner
2571 {
2572 return segment_ids_set.contains(&owner_line_id);
2573 }
2574 false
2575 }),
2576 Constraint::Radius(r) => segment_ids_set.contains(&r.arc),
2577 Constraint::Diameter(d) => segment_ids_set.contains(&d.arc),
2578 Constraint::HorizontalDistance(d) => d.points.iter().any(|pt_id| {
2579 let pt_object = self.scene_graph.objects.get(pt_id.0);
2580 if let Some(obj) = pt_object
2581 && let ObjectKind::Segment { segment } = &obj.kind
2582 && let Segment::Point(pt) = segment
2583 && let Some(owner_line_id) = pt.owner
2584 {
2585 return segment_ids_set.contains(&owner_line_id);
2586 }
2587 false
2588 }),
2589 Constraint::VerticalDistance(d) => d.points.iter().any(|pt_id| {
2590 let pt_object = self.scene_graph.objects.get(pt_id.0);
2591 if let Some(obj) = pt_object
2592 && let ObjectKind::Segment { segment } = &obj.kind
2593 && let Segment::Point(pt) = segment
2594 && let Some(owner_line_id) = pt.owner
2595 {
2596 return segment_ids_set.contains(&owner_line_id);
2597 }
2598 false
2599 }),
2600 Constraint::Horizontal(h) => segment_ids_set.contains(&h.line),
2601 Constraint::Vertical(v) => segment_ids_set.contains(&v.line),
2602 Constraint::LinesEqualLength(lines_equal_length) => lines_equal_length
2603 .lines
2604 .iter()
2605 .any(|line_id| segment_ids_set.contains(line_id)),
2606 Constraint::Parallel(parallel) => {
2607 parallel.lines.iter().any(|line_id| segment_ids_set.contains(line_id))
2608 }
2609 Constraint::Perpendicular(perpendicular) => perpendicular
2610 .lines
2611 .iter()
2612 .any(|line_id| segment_ids_set.contains(line_id)),
2613 };
2614 if depends_on_segment {
2615 constraint_ids_set.insert(*constraint_id);
2616 }
2617 }
2618 Ok(())
2619 }
2620
2621 fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
2622 #[cfg(not(feature = "artifact-graph"))]
2623 {
2624 let _ = freedom_analysis_ran; outcome
2626 }
2627 #[cfg(feature = "artifact-graph")]
2628 {
2629 let mut outcome = outcome;
2630 let new_objects = std::mem::take(&mut outcome.scene_objects);
2631
2632 if freedom_analysis_ran {
2633 self.point_freedom_cache.clear();
2636 for new_obj in &new_objects {
2637 if let ObjectKind::Segment {
2638 segment: crate::front::Segment::Point(point),
2639 } = &new_obj.kind
2640 {
2641 self.point_freedom_cache.insert(new_obj.id, point.freedom);
2642 }
2643 }
2644 self.scene_graph.objects = new_objects;
2646 } else {
2647 for old_obj in &self.scene_graph.objects {
2650 if let ObjectKind::Segment {
2651 segment: crate::front::Segment::Point(point),
2652 } = &old_obj.kind
2653 {
2654 self.point_freedom_cache.insert(old_obj.id, point.freedom);
2655 }
2656 }
2657
2658 let mut updated_objects = Vec::with_capacity(new_objects.len());
2660 for new_obj in new_objects {
2661 let mut obj = new_obj;
2662 if let ObjectKind::Segment {
2663 segment: crate::front::Segment::Point(point),
2664 } = &mut obj.kind
2665 {
2666 let new_freedom = point.freedom;
2667 match new_freedom {
2673 Freedom::Free => {
2674 match self.point_freedom_cache.get(&obj.id).copied() {
2675 Some(Freedom::Conflict) => {
2676 }
2679 Some(Freedom::Fixed) => {
2680 point.freedom = Freedom::Fixed;
2682 }
2683 Some(Freedom::Free) => {
2684 }
2686 None => {
2687 }
2689 }
2690 }
2691 Freedom::Fixed => {
2692 }
2694 Freedom::Conflict => {
2695 }
2697 }
2698 self.point_freedom_cache.insert(obj.id, point.freedom);
2700 }
2701 updated_objects.push(obj);
2702 }
2703
2704 self.scene_graph.objects = updated_objects;
2705 }
2706 outcome
2707 }
2708 }
2709
2710 fn only_sketch_block(
2711 &self,
2712 sketch_id: ObjectId,
2713 edit_kind: ChangeKind,
2714 ast: &mut ast::Node<ast::Program>,
2715 ) -> api::Result<()> {
2716 let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| Error {
2717 msg: format!("Sketch not found: {sketch_id:?}"),
2718 })?;
2719 let ObjectKind::Sketch(_) = &sketch_object.kind else {
2720 return Err(Error {
2721 msg: format!("Object is not a sketch: {sketch_object:?}"),
2722 });
2723 };
2724 let sketch_block_range = expect_single_source_range(&sketch_object.source)?;
2725 only_sketch_block(ast, sketch_block_range, edit_kind)
2726 }
2727
2728 fn mutate_ast(
2729 &mut self,
2730 ast: &mut ast::Node<ast::Program>,
2731 object_id: ObjectId,
2732 command: AstMutateCommand,
2733 ) -> api::Result<(SourceRange, AstMutateCommandReturn)> {
2734 let sketch_object = self.scene_graph.objects.get(object_id.0).ok_or_else(|| Error {
2735 msg: format!("Object not found: {object_id:?}"),
2736 })?;
2737 match &sketch_object.source {
2738 SourceRef::Simple { range } => mutate_ast_node_by_source_range(ast, *range, command),
2739 SourceRef::BackTrace { .. } => Err(Error {
2740 msg: "BackTrace source refs not supported yet".to_owned(),
2741 }),
2742 }
2743 }
2744}
2745
2746fn expect_single_source_range(source_ref: &SourceRef) -> api::Result<SourceRange> {
2747 match source_ref {
2748 SourceRef::Simple { range } => Ok(*range),
2749 SourceRef::BackTrace { ranges } => {
2750 if ranges.len() != 1 {
2751 return Err(Error {
2752 msg: format!(
2753 "Expected single source range in SourceRef, got {}; ranges={ranges:#?}",
2754 ranges.len(),
2755 ),
2756 });
2757 }
2758 Ok(ranges[0])
2759 }
2760 }
2761}
2762
2763fn only_sketch_block(
2764 ast: &mut ast::Node<ast::Program>,
2765 sketch_block_range: SourceRange,
2766 edit_kind: ChangeKind,
2767) -> api::Result<()> {
2768 let r1 = sketch_block_range;
2769 let matches_range = |r2: SourceRange| -> bool {
2770 match edit_kind {
2773 ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
2774 ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
2776 ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
2777 ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
2779 }
2780 };
2781 let mut found = false;
2782 for item in ast.body.iter_mut() {
2783 match item {
2784 ast::BodyItem::ImportStatement(_) => {}
2785 ast::BodyItem::ExpressionStatement(node) => {
2786 if matches_range(SourceRange::from(&*node))
2787 && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
2788 {
2789 sketch_block.is_being_edited = true;
2790 found = true;
2791 break;
2792 }
2793 }
2794 ast::BodyItem::VariableDeclaration(node) => {
2795 if matches_range(SourceRange::from(&node.declaration.init))
2796 && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
2797 {
2798 sketch_block.is_being_edited = true;
2799 found = true;
2800 break;
2801 }
2802 }
2803 ast::BodyItem::TypeDeclaration(_) => {}
2804 ast::BodyItem::ReturnStatement(node) => {
2805 if matches_range(SourceRange::from(&node.argument))
2806 && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
2807 {
2808 sketch_block.is_being_edited = true;
2809 found = true;
2810 break;
2811 }
2812 }
2813 }
2814 }
2815 if !found {
2816 return Err(Error {
2817 msg: format!("Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"),
2818 });
2819 }
2820
2821 Ok(())
2822}
2823
2824fn get_or_insert_ast_reference(
2831 ast: &mut ast::Node<ast::Program>,
2832 source_ref: &SourceRef,
2833 prefix: &str,
2834 property: Option<&str>,
2835) -> api::Result<ast::Expr> {
2836 let range = expect_single_source_range(source_ref)?;
2837 let command = AstMutateCommand::AddVariableDeclaration {
2838 prefix: prefix.to_owned(),
2839 };
2840 let (_, ret) = mutate_ast_node_by_source_range(ast, range, command)?;
2841 let AstMutateCommandReturn::Name(var_name) = ret else {
2842 return Err(Error {
2843 msg: "Expected variable name returned from AddVariableDeclaration".to_owned(),
2844 });
2845 };
2846 let var_expr = ast::Expr::Name(Box::new(ast::Name::new(&var_name)));
2847 let Some(property) = property else {
2848 return Ok(var_expr);
2850 };
2851
2852 Ok(create_member_expression(var_expr, property))
2853}
2854
2855fn mutate_ast_node_by_source_range(
2856 ast: &mut ast::Node<ast::Program>,
2857 source_range: SourceRange,
2858 command: AstMutateCommand,
2859) -> Result<(SourceRange, AstMutateCommandReturn), Error> {
2860 let mut context = AstMutateContext {
2861 source_range,
2862 command,
2863 defined_names_stack: Default::default(),
2864 };
2865 let control = dfs_mut(ast, &mut context);
2866 match control {
2867 ControlFlow::Continue(_) => Err(Error {
2868 msg: format!("Source range not found: {source_range:?}"),
2869 }),
2870 ControlFlow::Break(break_value) => break_value,
2871 }
2872}
2873
2874#[derive(Debug)]
2875struct AstMutateContext {
2876 source_range: SourceRange,
2877 command: AstMutateCommand,
2878 defined_names_stack: Vec<HashSet<String>>,
2879}
2880
2881#[derive(Debug)]
2882#[allow(clippy::large_enum_variant)]
2883enum AstMutateCommand {
2884 AddSketchBlockExprStmt {
2886 expr: ast::Expr,
2887 },
2888 AddVariableDeclaration {
2889 prefix: String,
2890 },
2891 EditPoint {
2892 at: ast::Expr,
2893 },
2894 EditLine {
2895 start: ast::Expr,
2896 end: ast::Expr,
2897 construction: Option<bool>,
2898 },
2899 EditArc {
2900 start: ast::Expr,
2901 end: ast::Expr,
2902 center: ast::Expr,
2903 construction: Option<bool>,
2904 },
2905 #[cfg(feature = "artifact-graph")]
2906 EditVarInitialValue {
2907 value: Number,
2908 },
2909 DeleteNode,
2910}
2911
2912#[derive(Debug)]
2913enum AstMutateCommandReturn {
2914 None,
2915 Name(String),
2916}
2917
2918impl Visitor for AstMutateContext {
2919 type Break = Result<(SourceRange, AstMutateCommandReturn), Error>;
2920 type Continue = ();
2921
2922 fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
2923 filter_and_process(self, node)
2924 }
2925
2926 fn finish(&mut self, node: NodeMut<'_>) {
2927 match &node {
2928 NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
2929 self.defined_names_stack.pop();
2930 }
2931 _ => {}
2932 }
2933 }
2934}
2935
2936fn filter_and_process(
2937 ctx: &mut AstMutateContext,
2938 node: NodeMut,
2939) -> TraversalReturn<Result<(SourceRange, AstMutateCommandReturn), Error>> {
2940 let Ok(node_range) = SourceRange::try_from(&node) else {
2941 return TraversalReturn::new_continue(());
2943 };
2944 if let NodeMut::VariableDeclaration(var_decl) = &node {
2949 let expr_range = SourceRange::from(&var_decl.declaration.init);
2950 if expr_range == ctx.source_range {
2951 if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
2952 return TraversalReturn::new_break(Ok((
2955 node_range,
2956 AstMutateCommandReturn::Name(var_decl.name().to_owned()),
2957 )));
2958 }
2959 if let AstMutateCommand::DeleteNode = &ctx.command {
2960 return TraversalReturn {
2963 mutate_body_item: MutateBodyItem::Delete,
2964 control_flow: ControlFlow::Break(Ok((ctx.source_range, AstMutateCommandReturn::None))),
2965 };
2966 }
2967 }
2968 }
2969
2970 if let NodeMut::Program(program) = &node {
2971 ctx.defined_names_stack.push(find_defined_names(*program));
2972 } else if let NodeMut::SketchBlock(block) = &node {
2973 ctx.defined_names_stack.push(find_defined_names(&block.body));
2974 }
2975
2976 if node_range != ctx.source_range {
2978 return TraversalReturn::new_continue(());
2979 }
2980 process(ctx, node).map_break(|result| result.map(|cmd_return| (ctx.source_range, cmd_return)))
2981}
2982
2983fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, Error>> {
2984 match &ctx.command {
2985 AstMutateCommand::AddSketchBlockExprStmt { expr } => {
2986 if let NodeMut::SketchBlock(sketch_block) = node {
2987 sketch_block
2988 .body
2989 .items
2990 .push(ast::BodyItem::ExpressionStatement(ast::Node {
2991 inner: ast::ExpressionStatement {
2992 expression: expr.clone(),
2993 digest: None,
2994 },
2995 start: Default::default(),
2996 end: Default::default(),
2997 module_id: Default::default(),
2998 outer_attrs: Default::default(),
2999 pre_comments: Default::default(),
3000 comment_start: Default::default(),
3001 }));
3002 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3003 }
3004 }
3005 AstMutateCommand::AddVariableDeclaration { prefix } => {
3006 if let NodeMut::VariableDeclaration(inner) = node {
3007 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
3008 }
3009 if let NodeMut::ExpressionStatement(expr_stmt) = node {
3010 let empty_defined_names = HashSet::new();
3011 let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
3012 let Ok(name) = next_free_name(prefix, defined_names) else {
3013 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3015 };
3016 let mutate_node =
3017 ast::BodyItem::VariableDeclaration(Box::new(ast::Node::no_src(ast::VariableDeclaration::new(
3018 ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
3019 ast::ItemVisibility::Default,
3020 ast::VariableKind::Const,
3021 ))));
3022 return TraversalReturn {
3023 mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
3024 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
3025 };
3026 }
3027 }
3028 AstMutateCommand::EditPoint { at } => {
3029 if let NodeMut::CallExpressionKw(call) = node {
3030 if call.callee.name.name != POINT_FN {
3031 return TraversalReturn::new_continue(());
3032 }
3033 for labeled_arg in &mut call.arguments {
3035 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
3036 labeled_arg.arg = at.clone();
3037 }
3038 }
3039 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3040 }
3041 }
3042 AstMutateCommand::EditLine {
3043 start,
3044 end,
3045 construction,
3046 } => {
3047 if let NodeMut::CallExpressionKw(call) = node {
3048 if call.callee.name.name != LINE_FN {
3049 return TraversalReturn::new_continue(());
3050 }
3051 for labeled_arg in &mut call.arguments {
3053 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
3054 labeled_arg.arg = start.clone();
3055 }
3056 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
3057 labeled_arg.arg = end.clone();
3058 }
3059 }
3060 if let Some(construction_value) = construction {
3062 let construction_exists = call
3063 .arguments
3064 .iter()
3065 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
3066 if *construction_value {
3067 if construction_exists {
3069 for labeled_arg in &mut call.arguments {
3071 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
3072 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
3073 value: ast::LiteralValue::Bool(true),
3074 raw: "true".to_string(),
3075 digest: None,
3076 })));
3077 }
3078 }
3079 } else {
3080 call.arguments.push(ast::LabeledArg {
3082 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
3083 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
3084 value: ast::LiteralValue::Bool(true),
3085 raw: "true".to_string(),
3086 digest: None,
3087 }))),
3088 });
3089 }
3090 } else {
3091 call.arguments
3093 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
3094 }
3095 }
3096 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3097 }
3098 }
3099 AstMutateCommand::EditArc {
3100 start,
3101 end,
3102 center,
3103 construction,
3104 } => {
3105 if let NodeMut::CallExpressionKw(call) = node {
3106 if call.callee.name.name != ARC_FN {
3107 return TraversalReturn::new_continue(());
3108 }
3109 for labeled_arg in &mut call.arguments {
3111 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
3112 labeled_arg.arg = start.clone();
3113 }
3114 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
3115 labeled_arg.arg = end.clone();
3116 }
3117 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
3118 labeled_arg.arg = center.clone();
3119 }
3120 }
3121 if let Some(construction_value) = construction {
3123 let construction_exists = call
3124 .arguments
3125 .iter()
3126 .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
3127 if *construction_value {
3128 if construction_exists {
3130 for labeled_arg in &mut call.arguments {
3132 if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
3133 labeled_arg.arg = ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
3134 value: ast::LiteralValue::Bool(true),
3135 raw: "true".to_string(),
3136 digest: None,
3137 })));
3138 }
3139 }
3140 } else {
3141 call.arguments.push(ast::LabeledArg {
3143 label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
3144 arg: ast::Expr::Literal(Box::new(ast::Node::no_src(ast::Literal {
3145 value: ast::LiteralValue::Bool(true),
3146 raw: "true".to_string(),
3147 digest: None,
3148 }))),
3149 });
3150 }
3151 } else {
3152 call.arguments
3154 .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
3155 }
3156 }
3157 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3158 }
3159 }
3160 #[cfg(feature = "artifact-graph")]
3161 AstMutateCommand::EditVarInitialValue { value } => {
3162 if let NodeMut::NumericLiteral(numeric_literal) = node {
3163 let Ok(literal) = to_source_number(*value) else {
3165 return TraversalReturn::new_break(Err(Error {
3166 msg: format!("Could not convert number to AST literal: {:?}", *value),
3167 }));
3168 };
3169 *numeric_literal = ast::Node::no_src(literal);
3170 return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
3171 }
3172 }
3173 AstMutateCommand::DeleteNode => {
3174 return TraversalReturn {
3175 mutate_body_item: MutateBodyItem::Delete,
3176 control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
3177 };
3178 }
3179 }
3180 TraversalReturn::new_continue(())
3181}
3182
3183struct FindSketchBlockSourceRange {
3184 target_before_mutation: SourceRange,
3186 found: Cell<Option<SourceRange>>,
3190}
3191
3192impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
3193 type Error = crate::front::Error;
3194
3195 fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
3196 let Ok(node_range) = SourceRange::try_from(&node) else {
3197 return Ok(true);
3198 };
3199
3200 if let crate::walk::Node::SketchBlock(sketch_block) = node {
3201 if node_range.module_id() == self.target_before_mutation.module_id()
3202 && node_range.start() == self.target_before_mutation.start()
3203 && node_range.end() >= self.target_before_mutation.end()
3205 {
3206 self.found.set(sketch_block.body.items.last().map(SourceRange::from));
3207 return Ok(false);
3208 } else {
3209 return Ok(true);
3212 }
3213 }
3214
3215 for child in node.children().iter() {
3216 if !child.visit(*self)? {
3217 return Ok(false);
3218 }
3219 }
3220
3221 Ok(true)
3222 }
3223}
3224
3225fn find_sketch_block_added_item(
3233 ast: &ast::Node<ast::Program>,
3234 range_before_mutation: SourceRange,
3235) -> api::Result<SourceRange> {
3236 let find = FindSketchBlockSourceRange {
3237 target_before_mutation: range_before_mutation,
3238 found: Cell::new(None),
3239 };
3240 let node = crate::walk::Node::from(ast);
3241 node.visit(&find)?;
3242 find.found.into_inner().ok_or_else(|| api::Error {
3243 msg: format!("Source range after mutation not found for range before mutation: {range_before_mutation:?}; Did you try formatting (i.e. call recast) before calling this?"),
3244 })
3245}
3246
3247fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
3248 ast.recast_top(&Default::default(), 0)
3250}
3251
3252pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
3253 Ok(ast::Expr::ArrayExpression(Box::new(ast::Node {
3254 inner: ast::ArrayExpression {
3255 elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
3256 non_code_meta: Default::default(),
3257 digest: None,
3258 },
3259 start: Default::default(),
3260 end: Default::default(),
3261 module_id: Default::default(),
3262 outer_attrs: Default::default(),
3263 pre_comments: Default::default(),
3264 comment_start: Default::default(),
3265 })))
3266}
3267
3268fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
3269 match expr {
3270 Expr::Number(number) => Ok(ast::Expr::Literal(Box::new(ast::Node {
3271 inner: ast::Literal::from(to_source_number(*number)?),
3272 start: Default::default(),
3273 end: Default::default(),
3274 module_id: Default::default(),
3275 outer_attrs: Default::default(),
3276 pre_comments: Default::default(),
3277 comment_start: Default::default(),
3278 }))),
3279 Expr::Var(number) => Ok(ast::Expr::SketchVar(Box::new(ast::Node {
3280 inner: ast::SketchVar {
3281 initial: Some(Box::new(ast::Node {
3282 inner: to_source_number(*number)?,
3283 start: Default::default(),
3284 end: Default::default(),
3285 module_id: Default::default(),
3286 outer_attrs: Default::default(),
3287 pre_comments: Default::default(),
3288 comment_start: Default::default(),
3289 })),
3290 digest: None,
3291 },
3292 start: Default::default(),
3293 end: Default::default(),
3294 module_id: Default::default(),
3295 outer_attrs: Default::default(),
3296 pre_comments: Default::default(),
3297 comment_start: Default::default(),
3298 }))),
3299 Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
3300 }
3301}
3302
3303fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
3304 Ok(ast::NumericLiteral {
3305 value: number.value,
3306 suffix: number.units,
3307 raw: format_number_literal(number.value, number.units)?,
3308 digest: None,
3309 })
3310}
3311
3312pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
3313 ast::Expr::Name(Box::new(ast_name(name)))
3314}
3315
3316fn ast_name(name: String) -> ast::Node<ast::Name> {
3317 ast::Node {
3318 inner: ast::Name {
3319 name: ast::Node {
3320 inner: ast::Identifier { name, digest: None },
3321 start: Default::default(),
3322 end: Default::default(),
3323 module_id: Default::default(),
3324 outer_attrs: Default::default(),
3325 pre_comments: Default::default(),
3326 comment_start: Default::default(),
3327 },
3328 path: Vec::new(),
3329 abs_path: false,
3330 digest: None,
3331 },
3332 start: Default::default(),
3333 end: Default::default(),
3334 module_id: Default::default(),
3335 outer_attrs: Default::default(),
3336 pre_comments: Default::default(),
3337 comment_start: Default::default(),
3338 }
3339}
3340
3341pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
3342 ast::Name {
3343 name: ast::Node {
3344 inner: ast::Identifier {
3345 name: name.to_owned(),
3346 digest: None,
3347 },
3348 start: Default::default(),
3349 end: Default::default(),
3350 module_id: Default::default(),
3351 outer_attrs: Default::default(),
3352 pre_comments: Default::default(),
3353 comment_start: Default::default(),
3354 },
3355 path: vec![ast::Node::no_src(ast::Identifier {
3356 name: "sketch2".to_owned(),
3357 digest: None,
3358 })],
3359 abs_path: false,
3360 digest: None,
3361 }
3362}
3363
3364pub(crate) fn create_coincident_ast(expr1: ast::Expr, expr2: ast::Expr) -> ast::Expr {
3368 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3370 elements: vec![expr1, expr2],
3371 digest: None,
3372 non_code_meta: Default::default(),
3373 })));
3374
3375 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3377 callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
3378 unlabeled: Some(array_expr),
3379 arguments: Default::default(),
3380 digest: None,
3381 non_code_meta: Default::default(),
3382 })))
3383}
3384
3385pub(crate) fn create_line_ast(start_ast: ast::Expr, end_ast: ast::Expr) -> ast::Expr {
3387 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3388 callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
3389 unlabeled: None,
3390 arguments: vec![
3391 ast::LabeledArg {
3392 label: Some(ast::Identifier::new(LINE_START_PARAM)),
3393 arg: start_ast,
3394 },
3395 ast::LabeledArg {
3396 label: Some(ast::Identifier::new(LINE_END_PARAM)),
3397 arg: end_ast,
3398 },
3399 ],
3400 digest: None,
3401 non_code_meta: Default::default(),
3402 })))
3403}
3404
3405pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
3407 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3408 callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
3409 unlabeled: Some(line_expr),
3410 arguments: Default::default(),
3411 digest: None,
3412 non_code_meta: Default::default(),
3413 })))
3414}
3415
3416pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
3418 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3419 callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
3420 unlabeled: Some(line_expr),
3421 arguments: Default::default(),
3422 digest: None,
3423 non_code_meta: Default::default(),
3424 })))
3425}
3426
3427pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
3429 ast::Expr::MemberExpression(Box::new(ast::Node::no_src(ast::MemberExpression {
3430 object: object_expr,
3431 property: ast::Expr::Name(Box::new(ast::Node::no_src(ast::Name {
3432 name: ast::Node::no_src(ast::Identifier {
3433 name: property.to_string(),
3434 digest: None,
3435 }),
3436 path: Vec::new(),
3437 abs_path: false,
3438 digest: None,
3439 }))),
3440 computed: false,
3441 digest: None,
3442 })))
3443}
3444
3445pub(crate) fn create_equal_length_ast(line1_expr: ast::Expr, line2_expr: ast::Expr) -> ast::Expr {
3447 let array_expr = ast::Expr::ArrayExpression(Box::new(ast::Node::no_src(ast::ArrayExpression {
3449 elements: vec![line1_expr, line2_expr],
3450 digest: None,
3451 non_code_meta: Default::default(),
3452 })));
3453
3454 ast::Expr::CallExpressionKw(Box::new(ast::Node::no_src(ast::CallExpressionKw {
3456 callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
3457 unlabeled: Some(array_expr),
3458 arguments: Default::default(),
3459 digest: None,
3460 non_code_meta: Default::default(),
3461 })))
3462}
3463
3464#[cfg(test)]
3465mod tests {
3466 use super::*;
3467 use crate::{
3468 engine::PlaneName,
3469 front::{Distance, Object, Plane, Sketch},
3470 frontend::sketch::Vertical,
3471 pretty::NumericSuffix,
3472 };
3473
3474 fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
3475 for object in &scene_graph.objects {
3476 if let ObjectKind::Sketch(_) = &object.kind {
3477 return Some(object);
3478 }
3479 }
3480 None
3481 }
3482
3483 fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
3484 for object in &scene_graph.objects {
3485 if let ObjectKind::Face(_) = &object.kind {
3486 return Some(object);
3487 }
3488 }
3489 None
3490 }
3491
3492 #[track_caller]
3493 fn expect_sketch(object: &Object) -> &Sketch {
3494 if let ObjectKind::Sketch(sketch) = &object.kind {
3495 sketch
3496 } else {
3497 panic!("Object is not a sketch: {:?}", object);
3498 }
3499 }
3500
3501 #[tokio::test(flavor = "multi_thread")]
3502 async fn test_new_sketch_add_point_edit_point() {
3503 let program = Program::empty();
3504
3505 let mut frontend = FrontendState::new();
3506 frontend.program = program;
3507
3508 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3509 let mock_ctx = ExecutorContext::new_mock(None).await;
3510 let version = Version(0);
3511
3512 let sketch_args = SketchCtor {
3513 on: PlaneName::Xy.to_string(),
3514 };
3515 let (_src_delta, scene_delta, sketch_id) = frontend
3516 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3517 .await
3518 .unwrap();
3519 assert_eq!(sketch_id, ObjectId(1));
3520 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3521 let sketch_object = &scene_delta.new_graph.objects[1];
3522 assert_eq!(sketch_object.id, ObjectId(1));
3523 assert_eq!(
3524 sketch_object.kind,
3525 ObjectKind::Sketch(Sketch {
3526 args: SketchCtor {
3527 on: PlaneName::Xy.to_string()
3528 },
3529 plane: ObjectId(0),
3530 segments: vec![],
3531 constraints: vec![],
3532 })
3533 );
3534 assert_eq!(scene_delta.new_graph.objects.len(), 2);
3535
3536 let point_ctor = PointCtor {
3537 position: Point2d {
3538 x: Expr::Number(Number {
3539 value: 1.0,
3540 units: NumericSuffix::Inch,
3541 }),
3542 y: Expr::Number(Number {
3543 value: 2.0,
3544 units: NumericSuffix::Inch,
3545 }),
3546 },
3547 };
3548 let segment = SegmentCtor::Point(point_ctor);
3549 let (src_delta, scene_delta) = frontend
3550 .add_segment(&mock_ctx, version, sketch_id, segment, None)
3551 .await
3552 .unwrap();
3553 assert_eq!(
3554 src_delta.text.as_str(),
3555 "@settings(experimentalFeatures = allow)
3556
3557sketch(on = XY) {
3558 sketch2::point(at = [1in, 2in])
3559}
3560"
3561 );
3562 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
3563 assert_eq!(scene_delta.new_graph.objects.len(), 3);
3564 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
3565 assert_eq!(scene_object.id.0, i);
3566 }
3567
3568 let point_id = *scene_delta.new_objects.last().unwrap();
3569
3570 let point_ctor = PointCtor {
3571 position: Point2d {
3572 x: Expr::Number(Number {
3573 value: 3.0,
3574 units: NumericSuffix::Inch,
3575 }),
3576 y: Expr::Number(Number {
3577 value: 4.0,
3578 units: NumericSuffix::Inch,
3579 }),
3580 },
3581 };
3582 let segments = vec![ExistingSegmentCtor {
3583 id: point_id,
3584 ctor: SegmentCtor::Point(point_ctor),
3585 }];
3586 let (src_delta, scene_delta) = frontend
3587 .edit_segments(&mock_ctx, version, sketch_id, segments)
3588 .await
3589 .unwrap();
3590 assert_eq!(
3591 src_delta.text.as_str(),
3592 "@settings(experimentalFeatures = allow)
3593
3594sketch(on = XY) {
3595 sketch2::point(at = [3in, 4in])
3596}
3597"
3598 );
3599 assert_eq!(scene_delta.new_objects, vec![]);
3600 assert_eq!(scene_delta.new_graph.objects.len(), 3);
3601
3602 ctx.close().await;
3603 mock_ctx.close().await;
3604 }
3605
3606 #[tokio::test(flavor = "multi_thread")]
3607 async fn test_new_sketch_add_line_edit_line() {
3608 let program = Program::empty();
3609
3610 let mut frontend = FrontendState::new();
3611 frontend.program = program;
3612
3613 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3614 let mock_ctx = ExecutorContext::new_mock(None).await;
3615 let version = Version(0);
3616
3617 let sketch_args = SketchCtor {
3618 on: PlaneName::Xy.to_string(),
3619 };
3620 let (_src_delta, scene_delta, sketch_id) = frontend
3621 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3622 .await
3623 .unwrap();
3624 assert_eq!(sketch_id, ObjectId(1));
3625 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3626 let sketch_object = &scene_delta.new_graph.objects[1];
3627 assert_eq!(sketch_object.id, ObjectId(1));
3628 assert_eq!(
3629 sketch_object.kind,
3630 ObjectKind::Sketch(Sketch {
3631 args: SketchCtor {
3632 on: PlaneName::Xy.to_string()
3633 },
3634 plane: ObjectId(0),
3635 segments: vec![],
3636 constraints: vec![],
3637 })
3638 );
3639 assert_eq!(scene_delta.new_graph.objects.len(), 2);
3640
3641 let line_ctor = LineCtor {
3642 start: Point2d {
3643 x: Expr::Number(Number {
3644 value: 0.0,
3645 units: NumericSuffix::Mm,
3646 }),
3647 y: Expr::Number(Number {
3648 value: 0.0,
3649 units: NumericSuffix::Mm,
3650 }),
3651 },
3652 end: Point2d {
3653 x: Expr::Number(Number {
3654 value: 10.0,
3655 units: NumericSuffix::Mm,
3656 }),
3657 y: Expr::Number(Number {
3658 value: 10.0,
3659 units: NumericSuffix::Mm,
3660 }),
3661 },
3662 construction: None,
3663 };
3664 let segment = SegmentCtor::Line(line_ctor);
3665 let (src_delta, scene_delta) = frontend
3666 .add_segment(&mock_ctx, version, sketch_id, segment, None)
3667 .await
3668 .unwrap();
3669 assert_eq!(
3670 src_delta.text.as_str(),
3671 "@settings(experimentalFeatures = allow)
3672
3673sketch(on = XY) {
3674 sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
3675}
3676"
3677 );
3678 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
3679 assert_eq!(scene_delta.new_graph.objects.len(), 5);
3680 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
3681 assert_eq!(scene_object.id.0, i);
3682 }
3683
3684 let line = *scene_delta.new_objects.last().unwrap();
3686
3687 let line_ctor = LineCtor {
3688 start: Point2d {
3689 x: Expr::Number(Number {
3690 value: 1.0,
3691 units: NumericSuffix::Mm,
3692 }),
3693 y: Expr::Number(Number {
3694 value: 2.0,
3695 units: NumericSuffix::Mm,
3696 }),
3697 },
3698 end: Point2d {
3699 x: Expr::Number(Number {
3700 value: 13.0,
3701 units: NumericSuffix::Mm,
3702 }),
3703 y: Expr::Number(Number {
3704 value: 14.0,
3705 units: NumericSuffix::Mm,
3706 }),
3707 },
3708 construction: None,
3709 };
3710 let segments = vec![ExistingSegmentCtor {
3711 id: line,
3712 ctor: SegmentCtor::Line(line_ctor),
3713 }];
3714 let (src_delta, scene_delta) = frontend
3715 .edit_segments(&mock_ctx, version, sketch_id, segments)
3716 .await
3717 .unwrap();
3718 assert_eq!(
3719 src_delta.text.as_str(),
3720 "@settings(experimentalFeatures = allow)
3721
3722sketch(on = XY) {
3723 sketch2::line(start = [1mm, 2mm], end = [13mm, 14mm])
3724}
3725"
3726 );
3727 assert_eq!(scene_delta.new_objects, vec![]);
3728 assert_eq!(scene_delta.new_graph.objects.len(), 5);
3729
3730 ctx.close().await;
3731 mock_ctx.close().await;
3732 }
3733
3734 #[tokio::test(flavor = "multi_thread")]
3735 async fn test_new_sketch_add_arc_edit_arc() {
3736 let program = Program::empty();
3737
3738 let mut frontend = FrontendState::new();
3739 frontend.program = program;
3740
3741 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3742 let mock_ctx = ExecutorContext::new_mock(None).await;
3743 let version = Version(0);
3744
3745 let sketch_args = SketchCtor {
3746 on: PlaneName::Xy.to_string(),
3747 };
3748 let (_src_delta, scene_delta, sketch_id) = frontend
3749 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3750 .await
3751 .unwrap();
3752 assert_eq!(sketch_id, ObjectId(1));
3753 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3754 let sketch_object = &scene_delta.new_graph.objects[1];
3755 assert_eq!(sketch_object.id, ObjectId(1));
3756 assert_eq!(
3757 sketch_object.kind,
3758 ObjectKind::Sketch(Sketch {
3759 args: SketchCtor {
3760 on: PlaneName::Xy.to_string(),
3761 },
3762 plane: ObjectId(0),
3763 segments: vec![],
3764 constraints: vec![],
3765 })
3766 );
3767 assert_eq!(scene_delta.new_graph.objects.len(), 2);
3768
3769 let arc_ctor = ArcCtor {
3770 start: Point2d {
3771 x: Expr::Var(Number {
3772 value: 0.0,
3773 units: NumericSuffix::Mm,
3774 }),
3775 y: Expr::Var(Number {
3776 value: 0.0,
3777 units: NumericSuffix::Mm,
3778 }),
3779 },
3780 end: Point2d {
3781 x: Expr::Var(Number {
3782 value: 10.0,
3783 units: NumericSuffix::Mm,
3784 }),
3785 y: Expr::Var(Number {
3786 value: 10.0,
3787 units: NumericSuffix::Mm,
3788 }),
3789 },
3790 center: Point2d {
3791 x: Expr::Var(Number {
3792 value: 10.0,
3793 units: NumericSuffix::Mm,
3794 }),
3795 y: Expr::Var(Number {
3796 value: 0.0,
3797 units: NumericSuffix::Mm,
3798 }),
3799 },
3800 construction: None,
3801 };
3802 let segment = SegmentCtor::Arc(arc_ctor);
3803 let (src_delta, scene_delta) = frontend
3804 .add_segment(&mock_ctx, version, sketch_id, segment, None)
3805 .await
3806 .unwrap();
3807 assert_eq!(
3808 src_delta.text.as_str(),
3809 "@settings(experimentalFeatures = allow)
3810
3811sketch(on = XY) {
3812 sketch2::arc(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm], center = [var 10mm, var 0mm])
3813}
3814"
3815 );
3816 assert_eq!(
3817 scene_delta.new_objects,
3818 vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
3819 );
3820 for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
3821 assert_eq!(scene_object.id.0, i);
3822 }
3823 assert_eq!(scene_delta.new_graph.objects.len(), 6);
3824
3825 let arc = *scene_delta.new_objects.last().unwrap();
3827
3828 let arc_ctor = ArcCtor {
3829 start: Point2d {
3830 x: Expr::Var(Number {
3831 value: 1.0,
3832 units: NumericSuffix::Mm,
3833 }),
3834 y: Expr::Var(Number {
3835 value: 2.0,
3836 units: NumericSuffix::Mm,
3837 }),
3838 },
3839 end: Point2d {
3840 x: Expr::Var(Number {
3841 value: 13.0,
3842 units: NumericSuffix::Mm,
3843 }),
3844 y: Expr::Var(Number {
3845 value: 14.0,
3846 units: NumericSuffix::Mm,
3847 }),
3848 },
3849 center: Point2d {
3850 x: Expr::Var(Number {
3851 value: 13.0,
3852 units: NumericSuffix::Mm,
3853 }),
3854 y: Expr::Var(Number {
3855 value: 2.0,
3856 units: NumericSuffix::Mm,
3857 }),
3858 },
3859 construction: None,
3860 };
3861 let segments = vec![ExistingSegmentCtor {
3862 id: arc,
3863 ctor: SegmentCtor::Arc(arc_ctor),
3864 }];
3865 let (src_delta, scene_delta) = frontend
3866 .edit_segments(&mock_ctx, version, sketch_id, segments)
3867 .await
3868 .unwrap();
3869 assert_eq!(
3870 src_delta.text.as_str(),
3871 "@settings(experimentalFeatures = allow)
3872
3873sketch(on = XY) {
3874 sketch2::arc(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm], center = [var 13mm, var 2mm])
3875}
3876"
3877 );
3878 assert_eq!(scene_delta.new_objects, vec![]);
3879 assert_eq!(scene_delta.new_graph.objects.len(), 6);
3880
3881 ctx.close().await;
3882 mock_ctx.close().await;
3883 }
3884
3885 #[tokio::test(flavor = "multi_thread")]
3886 async fn test_add_line_when_sketch_block_uses_variable() {
3887 let initial_source = "@settings(experimentalFeatures = allow)
3888
3889s = sketch(on = XY) {}
3890";
3891
3892 let program = Program::parse(initial_source).unwrap().0.unwrap();
3893
3894 let mut frontend = FrontendState::new();
3895
3896 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3897 let mock_ctx = ExecutorContext::new_mock(None).await;
3898 let version = Version(0);
3899
3900 frontend.hack_set_program(&ctx, program).await.unwrap();
3901 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
3902 let sketch_id = sketch_object.id;
3903
3904 let line_ctor = LineCtor {
3905 start: Point2d {
3906 x: Expr::Number(Number {
3907 value: 0.0,
3908 units: NumericSuffix::Mm,
3909 }),
3910 y: Expr::Number(Number {
3911 value: 0.0,
3912 units: NumericSuffix::Mm,
3913 }),
3914 },
3915 end: Point2d {
3916 x: Expr::Number(Number {
3917 value: 10.0,
3918 units: NumericSuffix::Mm,
3919 }),
3920 y: Expr::Number(Number {
3921 value: 10.0,
3922 units: NumericSuffix::Mm,
3923 }),
3924 },
3925 construction: None,
3926 };
3927 let segment = SegmentCtor::Line(line_ctor);
3928 let (src_delta, scene_delta) = frontend
3929 .add_segment(&mock_ctx, version, sketch_id, segment, None)
3930 .await
3931 .unwrap();
3932 assert_eq!(
3933 src_delta.text.as_str(),
3934 "@settings(experimentalFeatures = allow)
3935
3936s = sketch(on = XY) {
3937 sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
3938}
3939"
3940 );
3941 assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
3942 assert_eq!(scene_delta.new_graph.objects.len(), 5);
3943
3944 ctx.close().await;
3945 mock_ctx.close().await;
3946 }
3947
3948 #[tokio::test(flavor = "multi_thread")]
3949 async fn test_new_sketch_add_line_delete_sketch() {
3950 let program = Program::empty();
3951
3952 let mut frontend = FrontendState::new();
3953 frontend.program = program;
3954
3955 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
3956 let mock_ctx = ExecutorContext::new_mock(None).await;
3957 let version = Version(0);
3958
3959 let sketch_args = SketchCtor {
3960 on: PlaneName::Xy.to_string(),
3961 };
3962 let (_src_delta, scene_delta, sketch_id) = frontend
3963 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
3964 .await
3965 .unwrap();
3966 assert_eq!(sketch_id, ObjectId(1));
3967 assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
3968 let sketch_object = &scene_delta.new_graph.objects[1];
3969 assert_eq!(sketch_object.id, ObjectId(1));
3970 assert_eq!(
3971 sketch_object.kind,
3972 ObjectKind::Sketch(Sketch {
3973 args: SketchCtor {
3974 on: PlaneName::Xy.to_string()
3975 },
3976 plane: ObjectId(0),
3977 segments: vec![],
3978 constraints: vec![],
3979 })
3980 );
3981 assert_eq!(scene_delta.new_graph.objects.len(), 2);
3982
3983 let line_ctor = LineCtor {
3984 start: Point2d {
3985 x: Expr::Number(Number {
3986 value: 0.0,
3987 units: NumericSuffix::Mm,
3988 }),
3989 y: Expr::Number(Number {
3990 value: 0.0,
3991 units: NumericSuffix::Mm,
3992 }),
3993 },
3994 end: Point2d {
3995 x: Expr::Number(Number {
3996 value: 10.0,
3997 units: NumericSuffix::Mm,
3998 }),
3999 y: Expr::Number(Number {
4000 value: 10.0,
4001 units: NumericSuffix::Mm,
4002 }),
4003 },
4004 construction: None,
4005 };
4006 let segment = SegmentCtor::Line(line_ctor);
4007 let (src_delta, scene_delta) = frontend
4008 .add_segment(&mock_ctx, version, sketch_id, segment, None)
4009 .await
4010 .unwrap();
4011 assert_eq!(
4012 src_delta.text.as_str(),
4013 "@settings(experimentalFeatures = allow)
4014
4015sketch(on = XY) {
4016 sketch2::line(start = [0mm, 0mm], end = [10mm, 10mm])
4017}
4018"
4019 );
4020 assert_eq!(scene_delta.new_graph.objects.len(), 5);
4021
4022 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
4023 assert_eq!(
4024 src_delta.text.as_str(),
4025 "@settings(experimentalFeatures = allow)
4026"
4027 );
4028 assert_eq!(scene_delta.new_graph.objects.len(), 0);
4029
4030 ctx.close().await;
4031 mock_ctx.close().await;
4032 }
4033
4034 #[tokio::test(flavor = "multi_thread")]
4035 async fn test_delete_sketch_when_sketch_block_uses_variable() {
4036 let initial_source = "@settings(experimentalFeatures = allow)
4037
4038s = sketch(on = XY) {}
4039";
4040
4041 let program = Program::parse(initial_source).unwrap().0.unwrap();
4042
4043 let mut frontend = FrontendState::new();
4044
4045 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4046 let mock_ctx = ExecutorContext::new_mock(None).await;
4047 let version = Version(0);
4048
4049 frontend.hack_set_program(&ctx, program).await.unwrap();
4050 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4051 let sketch_id = sketch_object.id;
4052
4053 let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
4054 assert_eq!(
4055 src_delta.text.as_str(),
4056 "@settings(experimentalFeatures = allow)
4057"
4058 );
4059 assert_eq!(scene_delta.new_graph.objects.len(), 0);
4060
4061 ctx.close().await;
4062 mock_ctx.close().await;
4063 }
4064
4065 #[tokio::test(flavor = "multi_thread")]
4066 async fn test_edit_line_when_editing_its_start_point() {
4067 let initial_source = "\
4068@settings(experimentalFeatures = allow)
4069
4070sketch(on = XY) {
4071 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4072}
4073";
4074
4075 let program = Program::parse(initial_source).unwrap().0.unwrap();
4076
4077 let mut frontend = FrontendState::new();
4078
4079 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4080 let mock_ctx = ExecutorContext::new_mock(None).await;
4081 let version = Version(0);
4082
4083 frontend.hack_set_program(&ctx, program).await.unwrap();
4084 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4085 let sketch_id = sketch_object.id;
4086 let sketch = expect_sketch(sketch_object);
4087
4088 let point_id = *sketch.segments.first().unwrap();
4089
4090 let point_ctor = PointCtor {
4091 position: Point2d {
4092 x: Expr::Var(Number {
4093 value: 5.0,
4094 units: NumericSuffix::Inch,
4095 }),
4096 y: Expr::Var(Number {
4097 value: 6.0,
4098 units: NumericSuffix::Inch,
4099 }),
4100 },
4101 };
4102 let segments = vec![ExistingSegmentCtor {
4103 id: point_id,
4104 ctor: SegmentCtor::Point(point_ctor),
4105 }];
4106 let (src_delta, scene_delta) = frontend
4107 .edit_segments(&mock_ctx, version, sketch_id, segments)
4108 .await
4109 .unwrap();
4110 assert_eq!(
4111 src_delta.text.as_str(),
4112 "\
4113@settings(experimentalFeatures = allow)
4114
4115sketch(on = XY) {
4116 sketch2::line(start = [var 127mm, var 152.4mm], end = [var 3mm, var 4mm])
4117}
4118"
4119 );
4120 assert_eq!(scene_delta.new_objects, vec![]);
4121 assert_eq!(scene_delta.new_graph.objects.len(), 5);
4122
4123 ctx.close().await;
4124 mock_ctx.close().await;
4125 }
4126
4127 #[tokio::test(flavor = "multi_thread")]
4128 async fn test_edit_line_when_editing_its_end_point() {
4129 let initial_source = "\
4130@settings(experimentalFeatures = allow)
4131
4132sketch(on = XY) {
4133 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4134}
4135";
4136
4137 let program = Program::parse(initial_source).unwrap().0.unwrap();
4138
4139 let mut frontend = FrontendState::new();
4140
4141 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4142 let mock_ctx = ExecutorContext::new_mock(None).await;
4143 let version = Version(0);
4144
4145 frontend.hack_set_program(&ctx, program).await.unwrap();
4146 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4147 let sketch_id = sketch_object.id;
4148 let sketch = expect_sketch(sketch_object);
4149 let point_id = *sketch.segments.get(1).unwrap();
4150
4151 let point_ctor = PointCtor {
4152 position: Point2d {
4153 x: Expr::Var(Number {
4154 value: 5.0,
4155 units: NumericSuffix::Inch,
4156 }),
4157 y: Expr::Var(Number {
4158 value: 6.0,
4159 units: NumericSuffix::Inch,
4160 }),
4161 },
4162 };
4163 let segments = vec![ExistingSegmentCtor {
4164 id: point_id,
4165 ctor: SegmentCtor::Point(point_ctor),
4166 }];
4167 let (src_delta, scene_delta) = frontend
4168 .edit_segments(&mock_ctx, version, sketch_id, segments)
4169 .await
4170 .unwrap();
4171 assert_eq!(
4172 src_delta.text.as_str(),
4173 "\
4174@settings(experimentalFeatures = allow)
4175
4176sketch(on = XY) {
4177 sketch2::line(start = [var 1mm, var 2mm], end = [var 127mm, var 152.4mm])
4178}
4179"
4180 );
4181 assert_eq!(scene_delta.new_objects, vec![]);
4182 assert_eq!(
4183 scene_delta.new_graph.objects.len(),
4184 5,
4185 "{:#?}",
4186 scene_delta.new_graph.objects
4187 );
4188
4189 ctx.close().await;
4190 mock_ctx.close().await;
4191 }
4192
4193 #[tokio::test(flavor = "multi_thread")]
4194 async fn test_edit_line_with_coincident_feedback() {
4195 let initial_source = "\
4196@settings(experimentalFeatures = allow)
4197
4198sketch(on = XY) {
4199 line1 = sketch2::line(start = [var 1, var 2], end = [var 1, var 2])
4200 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4201 line1.start.at[0] == 0
4202 line1.start.at[1] == 0
4203 sketch2::coincident([line1.end, line2.start])
4204 sketch2::equalLength([line1, line2])
4205}
4206";
4207
4208 let program = Program::parse(initial_source).unwrap().0.unwrap();
4209
4210 let mut frontend = FrontendState::new();
4211
4212 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4213 let mock_ctx = ExecutorContext::new_mock(None).await;
4214 let version = Version(0);
4215
4216 frontend.hack_set_program(&ctx, program).await.unwrap();
4217 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4218 let sketch_id = sketch_object.id;
4219 let sketch = expect_sketch(sketch_object);
4220 let line2_end_id = *sketch.segments.get(4).unwrap();
4221
4222 let segments = vec![ExistingSegmentCtor {
4223 id: line2_end_id,
4224 ctor: SegmentCtor::Point(PointCtor {
4225 position: Point2d {
4226 x: Expr::Var(Number {
4227 value: 9.0,
4228 units: NumericSuffix::None,
4229 }),
4230 y: Expr::Var(Number {
4231 value: 10.0,
4232 units: NumericSuffix::None,
4233 }),
4234 },
4235 }),
4236 }];
4237 let (src_delta, scene_delta) = frontend
4238 .edit_segments(&mock_ctx, version, sketch_id, segments)
4239 .await
4240 .unwrap();
4241 assert_eq!(
4242 src_delta.text.as_str(),
4243 "\
4244@settings(experimentalFeatures = allow)
4245
4246sketch(on = XY) {
4247 line1 = sketch2::line(start = [var 0mm, var 0mm], end = [var 4.14mm, var 5.32mm])
4248 line2 = sketch2::line(start = [var 4.14mm, var 5.32mm], end = [var 9mm, var 10mm])
4249line1.start.at[0] == 0
4250line1.start.at[1] == 0
4251 sketch2::coincident([line1.end, line2.start])
4252 sketch2::equalLength([line1, line2])
4253}
4254"
4255 );
4256 assert_eq!(
4257 scene_delta.new_graph.objects.len(),
4258 10,
4259 "{:#?}",
4260 scene_delta.new_graph.objects
4261 );
4262
4263 ctx.close().await;
4264 mock_ctx.close().await;
4265 }
4266
4267 #[tokio::test(flavor = "multi_thread")]
4268 async fn test_delete_point_without_var() {
4269 let initial_source = "\
4270@settings(experimentalFeatures = allow)
4271
4272sketch(on = XY) {
4273 sketch2::point(at = [var 1, var 2])
4274 sketch2::point(at = [var 3, var 4])
4275 sketch2::point(at = [var 5, var 6])
4276}
4277";
4278
4279 let program = Program::parse(initial_source).unwrap().0.unwrap();
4280
4281 let mut frontend = FrontendState::new();
4282
4283 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4284 let mock_ctx = ExecutorContext::new_mock(None).await;
4285 let version = Version(0);
4286
4287 frontend.hack_set_program(&ctx, program).await.unwrap();
4288 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4289 let sketch_id = sketch_object.id;
4290 let sketch = expect_sketch(sketch_object);
4291
4292 let point_id = *sketch.segments.get(1).unwrap();
4293
4294 let (src_delta, scene_delta) = frontend
4295 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
4296 .await
4297 .unwrap();
4298 assert_eq!(
4299 src_delta.text.as_str(),
4300 "\
4301@settings(experimentalFeatures = allow)
4302
4303sketch(on = XY) {
4304 sketch2::point(at = [var 1mm, var 2mm])
4305 sketch2::point(at = [var 5mm, var 6mm])
4306}
4307"
4308 );
4309 assert_eq!(scene_delta.new_objects, vec![]);
4310 assert_eq!(scene_delta.new_graph.objects.len(), 4);
4311
4312 ctx.close().await;
4313 mock_ctx.close().await;
4314 }
4315
4316 #[tokio::test(flavor = "multi_thread")]
4317 async fn test_delete_point_with_var() {
4318 let initial_source = "\
4319@settings(experimentalFeatures = allow)
4320
4321sketch(on = XY) {
4322 sketch2::point(at = [var 1, var 2])
4323 point1 = sketch2::point(at = [var 3, var 4])
4324 sketch2::point(at = [var 5, var 6])
4325}
4326";
4327
4328 let program = Program::parse(initial_source).unwrap().0.unwrap();
4329
4330 let mut frontend = FrontendState::new();
4331
4332 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4333 let mock_ctx = ExecutorContext::new_mock(None).await;
4334 let version = Version(0);
4335
4336 frontend.hack_set_program(&ctx, program).await.unwrap();
4337 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4338 let sketch_id = sketch_object.id;
4339 let sketch = expect_sketch(sketch_object);
4340
4341 let point_id = *sketch.segments.get(1).unwrap();
4342
4343 let (src_delta, scene_delta) = frontend
4344 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
4345 .await
4346 .unwrap();
4347 assert_eq!(
4348 src_delta.text.as_str(),
4349 "\
4350@settings(experimentalFeatures = allow)
4351
4352sketch(on = XY) {
4353 sketch2::point(at = [var 1mm, var 2mm])
4354 sketch2::point(at = [var 5mm, var 6mm])
4355}
4356"
4357 );
4358 assert_eq!(scene_delta.new_objects, vec![]);
4359 assert_eq!(scene_delta.new_graph.objects.len(), 4);
4360
4361 ctx.close().await;
4362 mock_ctx.close().await;
4363 }
4364
4365 #[tokio::test(flavor = "multi_thread")]
4366 async fn test_delete_multiple_points() {
4367 let initial_source = "\
4368@settings(experimentalFeatures = allow)
4369
4370sketch(on = XY) {
4371 sketch2::point(at = [var 1, var 2])
4372 point1 = sketch2::point(at = [var 3, var 4])
4373 sketch2::point(at = [var 5, var 6])
4374}
4375";
4376
4377 let program = Program::parse(initial_source).unwrap().0.unwrap();
4378
4379 let mut frontend = FrontendState::new();
4380
4381 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4382 let mock_ctx = ExecutorContext::new_mock(None).await;
4383 let version = Version(0);
4384
4385 frontend.hack_set_program(&ctx, program).await.unwrap();
4386 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4387 let sketch_id = sketch_object.id;
4388
4389 let sketch = expect_sketch(sketch_object);
4390
4391 let point1_id = *sketch.segments.first().unwrap();
4392 let point2_id = *sketch.segments.get(1).unwrap();
4393
4394 let (src_delta, scene_delta) = frontend
4395 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
4396 .await
4397 .unwrap();
4398 assert_eq!(
4399 src_delta.text.as_str(),
4400 "\
4401@settings(experimentalFeatures = allow)
4402
4403sketch(on = XY) {
4404 sketch2::point(at = [var 5mm, var 6mm])
4405}
4406"
4407 );
4408 assert_eq!(scene_delta.new_objects, vec![]);
4409 assert_eq!(scene_delta.new_graph.objects.len(), 3);
4410
4411 ctx.close().await;
4412 mock_ctx.close().await;
4413 }
4414
4415 #[tokio::test(flavor = "multi_thread")]
4416 async fn test_delete_coincident_constraint() {
4417 let initial_source = "\
4418@settings(experimentalFeatures = allow)
4419
4420sketch(on = XY) {
4421 point1 = sketch2::point(at = [var 1, var 2])
4422 point2 = sketch2::point(at = [var 3, var 4])
4423 sketch2::coincident([point1, point2])
4424 sketch2::point(at = [var 5, var 6])
4425}
4426";
4427
4428 let program = Program::parse(initial_source).unwrap().0.unwrap();
4429
4430 let mut frontend = FrontendState::new();
4431
4432 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4433 let mock_ctx = ExecutorContext::new_mock(None).await;
4434 let version = Version(0);
4435
4436 frontend.hack_set_program(&ctx, program).await.unwrap();
4437 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4438 let sketch_id = sketch_object.id;
4439 let sketch = expect_sketch(sketch_object);
4440
4441 let coincident_id = *sketch.constraints.first().unwrap();
4442
4443 let (src_delta, scene_delta) = frontend
4444 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
4445 .await
4446 .unwrap();
4447 assert_eq!(
4448 src_delta.text.as_str(),
4449 "\
4450@settings(experimentalFeatures = allow)
4451
4452sketch(on = XY) {
4453 point1 = sketch2::point(at = [var 1mm, var 2mm])
4454 point2 = sketch2::point(at = [var 3mm, var 4mm])
4455 sketch2::point(at = [var 5mm, var 6mm])
4456}
4457"
4458 );
4459 assert_eq!(scene_delta.new_objects, vec![]);
4460 assert_eq!(scene_delta.new_graph.objects.len(), 5);
4461
4462 ctx.close().await;
4463 mock_ctx.close().await;
4464 }
4465
4466 #[tokio::test(flavor = "multi_thread")]
4467 async fn test_delete_line_cascades_to_coincident_constraint() {
4468 let initial_source = "\
4469@settings(experimentalFeatures = allow)
4470
4471sketch(on = XY) {
4472 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4473 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4474 sketch2::coincident([line1.end, line2.start])
4475}
4476";
4477
4478 let program = Program::parse(initial_source).unwrap().0.unwrap();
4479
4480 let mut frontend = FrontendState::new();
4481
4482 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4483 let mock_ctx = ExecutorContext::new_mock(None).await;
4484 let version = Version(0);
4485
4486 frontend.hack_set_program(&ctx, program).await.unwrap();
4487 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4488 let sketch_id = sketch_object.id;
4489 let sketch = expect_sketch(sketch_object);
4490 let line_id = *sketch.segments.get(5).unwrap();
4491
4492 let (src_delta, scene_delta) = frontend
4493 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
4494 .await
4495 .unwrap();
4496 assert_eq!(
4497 src_delta.text.as_str(),
4498 "\
4499@settings(experimentalFeatures = allow)
4500
4501sketch(on = XY) {
4502 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4503}
4504"
4505 );
4506 assert_eq!(
4507 scene_delta.new_graph.objects.len(),
4508 5,
4509 "{:#?}",
4510 scene_delta.new_graph.objects
4511 );
4512
4513 ctx.close().await;
4514 mock_ctx.close().await;
4515 }
4516
4517 #[tokio::test(flavor = "multi_thread")]
4518 async fn test_delete_line_cascades_to_distance_constraint() {
4519 let initial_source = "\
4520@settings(experimentalFeatures = allow)
4521
4522sketch(on = XY) {
4523 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4524 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4525 sketch2::distance([line1.end, line2.start]) == 10mm
4526}
4527";
4528
4529 let program = Program::parse(initial_source).unwrap().0.unwrap();
4530
4531 let mut frontend = FrontendState::new();
4532
4533 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4534 let mock_ctx = ExecutorContext::new_mock(None).await;
4535 let version = Version(0);
4536
4537 frontend.hack_set_program(&ctx, program).await.unwrap();
4538 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4539 let sketch_id = sketch_object.id;
4540 let sketch = expect_sketch(sketch_object);
4541 let line_id = *sketch.segments.get(5).unwrap();
4542
4543 let (src_delta, scene_delta) = frontend
4544 .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
4545 .await
4546 .unwrap();
4547 assert_eq!(
4548 src_delta.text.as_str(),
4549 "\
4550@settings(experimentalFeatures = allow)
4551
4552sketch(on = XY) {
4553 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4554}
4555"
4556 );
4557 assert_eq!(
4558 scene_delta.new_graph.objects.len(),
4559 5,
4560 "{:#?}",
4561 scene_delta.new_graph.objects
4562 );
4563
4564 ctx.close().await;
4565 mock_ctx.close().await;
4566 }
4567
4568 #[tokio::test(flavor = "multi_thread")]
4569 async fn test_delete_line_line_coincident_constraint() {
4570 let initial_source = "\
4571@settings(experimentalFeatures = allow)
4572
4573sketch(on = XY) {
4574 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4575 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4576 sketch2::coincident([line1, line2])
4577}
4578";
4579
4580 let program = Program::parse(initial_source).unwrap().0.unwrap();
4581
4582 let mut frontend = FrontendState::new();
4583
4584 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4585 let mock_ctx = ExecutorContext::new_mock(None).await;
4586 let version = Version(0);
4587
4588 frontend.hack_set_program(&ctx, program).await.unwrap();
4589 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4590 let sketch_id = sketch_object.id;
4591 let sketch = expect_sketch(sketch_object);
4592
4593 let coincident_id = *sketch.constraints.first().unwrap();
4594
4595 let (src_delta, scene_delta) = frontend
4596 .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
4597 .await
4598 .unwrap();
4599 assert_eq!(
4600 src_delta.text.as_str(),
4601 "\
4602@settings(experimentalFeatures = allow)
4603
4604sketch(on = XY) {
4605 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
4606 line2 = sketch2::line(start = [var 5mm, var 6mm], end = [var 7mm, var 8mm])
4607}
4608"
4609 );
4610 assert_eq!(scene_delta.new_objects, vec![]);
4611 assert_eq!(scene_delta.new_graph.objects.len(), 8);
4612
4613 ctx.close().await;
4614 mock_ctx.close().await;
4615 }
4616
4617 #[tokio::test(flavor = "multi_thread")]
4618 async fn test_two_points_coincident() {
4619 let initial_source = "\
4620@settings(experimentalFeatures = allow)
4621
4622sketch(on = XY) {
4623 point1 = sketch2::point(at = [var 1, var 2])
4624 sketch2::point(at = [3, 4])
4625}
4626";
4627
4628 let program = Program::parse(initial_source).unwrap().0.unwrap();
4629
4630 let mut frontend = FrontendState::new();
4631
4632 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4633 let mock_ctx = ExecutorContext::new_mock(None).await;
4634 let version = Version(0);
4635
4636 frontend.hack_set_program(&ctx, program).await.unwrap();
4637 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4638 let sketch_id = sketch_object.id;
4639 let sketch = expect_sketch(sketch_object);
4640 let point0_id = *sketch.segments.first().unwrap();
4641 let point1_id = *sketch.segments.get(1).unwrap();
4642
4643 let constraint = Constraint::Coincident(Coincident {
4644 segments: vec![point0_id, point1_id],
4645 });
4646 let (src_delta, scene_delta) = frontend
4647 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4648 .await
4649 .unwrap();
4650 assert_eq!(
4651 src_delta.text.as_str(),
4652 "\
4653@settings(experimentalFeatures = allow)
4654
4655sketch(on = XY) {
4656 point1 = sketch2::point(at = [var 1, var 2])
4657 point2 = sketch2::point(at = [3, 4])
4658 sketch2::coincident([point1, point2])
4659}
4660"
4661 );
4662 assert_eq!(
4663 scene_delta.new_graph.objects.len(),
4664 5,
4665 "{:#?}",
4666 scene_delta.new_graph.objects
4667 );
4668
4669 ctx.close().await;
4670 mock_ctx.close().await;
4671 }
4672
4673 #[tokio::test(flavor = "multi_thread")]
4674 async fn test_coincident_of_line_end_points() {
4675 let initial_source = "\
4676@settings(experimentalFeatures = allow)
4677
4678sketch(on = XY) {
4679 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4680 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4681}
4682";
4683
4684 let program = Program::parse(initial_source).unwrap().0.unwrap();
4685
4686 let mut frontend = FrontendState::new();
4687
4688 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4689 let mock_ctx = ExecutorContext::new_mock(None).await;
4690 let version = Version(0);
4691
4692 frontend.hack_set_program(&ctx, program).await.unwrap();
4693 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4694 let sketch_id = sketch_object.id;
4695 let sketch = expect_sketch(sketch_object);
4696 let point0_id = *sketch.segments.get(1).unwrap();
4697 let point1_id = *sketch.segments.get(3).unwrap();
4698
4699 let constraint = Constraint::Coincident(Coincident {
4700 segments: vec![point0_id, point1_id],
4701 });
4702 let (src_delta, scene_delta) = frontend
4703 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4704 .await
4705 .unwrap();
4706 assert_eq!(
4707 src_delta.text.as_str(),
4708 "\
4709@settings(experimentalFeatures = allow)
4710
4711sketch(on = XY) {
4712 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
4713 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
4714 sketch2::coincident([line1.end, line2.start])
4715}
4716"
4717 );
4718 assert_eq!(
4719 scene_delta.new_graph.objects.len(),
4720 9,
4721 "{:#?}",
4722 scene_delta.new_graph.objects
4723 );
4724
4725 ctx.close().await;
4726 mock_ctx.close().await;
4727 }
4728
4729 #[tokio::test(flavor = "multi_thread")]
4730 async fn test_invalid_coincident_arc_and_line_preserves_state() {
4731 let program = Program::empty();
4739
4740 let mut frontend = FrontendState::new();
4741 frontend.program = program;
4742
4743 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4744 let mock_ctx = ExecutorContext::new_mock(None).await;
4745 let version = Version(0);
4746
4747 let sketch_args = SketchCtor {
4748 on: PlaneName::Xy.to_string(),
4749 };
4750 let (_src_delta, _scene_delta, sketch_id) = frontend
4751 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
4752 .await
4753 .unwrap();
4754
4755 let arc_ctor = ArcCtor {
4757 start: Point2d {
4758 x: Expr::Var(Number {
4759 value: 0.0,
4760 units: NumericSuffix::Mm,
4761 }),
4762 y: Expr::Var(Number {
4763 value: 0.0,
4764 units: NumericSuffix::Mm,
4765 }),
4766 },
4767 end: Point2d {
4768 x: Expr::Var(Number {
4769 value: 10.0,
4770 units: NumericSuffix::Mm,
4771 }),
4772 y: Expr::Var(Number {
4773 value: 10.0,
4774 units: NumericSuffix::Mm,
4775 }),
4776 },
4777 center: Point2d {
4778 x: Expr::Var(Number {
4779 value: 10.0,
4780 units: NumericSuffix::Mm,
4781 }),
4782 y: Expr::Var(Number {
4783 value: 0.0,
4784 units: NumericSuffix::Mm,
4785 }),
4786 },
4787 construction: None,
4788 };
4789 let (_src_delta, scene_delta) = frontend
4790 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
4791 .await
4792 .unwrap();
4793 let arc_id = *scene_delta.new_objects.last().unwrap();
4795
4796 let line_ctor = LineCtor {
4798 start: Point2d {
4799 x: Expr::Var(Number {
4800 value: 20.0,
4801 units: NumericSuffix::Mm,
4802 }),
4803 y: Expr::Var(Number {
4804 value: 0.0,
4805 units: NumericSuffix::Mm,
4806 }),
4807 },
4808 end: Point2d {
4809 x: Expr::Var(Number {
4810 value: 30.0,
4811 units: NumericSuffix::Mm,
4812 }),
4813 y: Expr::Var(Number {
4814 value: 10.0,
4815 units: NumericSuffix::Mm,
4816 }),
4817 },
4818 construction: None,
4819 };
4820 let (_src_delta, scene_delta) = frontend
4821 .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
4822 .await
4823 .unwrap();
4824 let line_id = *scene_delta.new_objects.last().unwrap();
4826
4827 let constraint = Constraint::Coincident(Coincident {
4830 segments: vec![arc_id, line_id],
4831 });
4832 let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
4833
4834 assert!(result.is_err(), "Expected invalid coincident constraint to fail");
4836
4837 let sketch_object_after =
4840 find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
4841 let sketch_after = expect_sketch(sketch_object_after);
4842
4843 assert!(
4845 sketch_after.segments.contains(&arc_id),
4846 "Arc segment should still exist after failed constraint"
4847 );
4848 assert!(
4849 sketch_after.segments.contains(&line_id),
4850 "Line segment should still exist after failed constraint"
4851 );
4852
4853 let arc_obj = frontend
4855 .scene_graph
4856 .objects
4857 .get(arc_id.0)
4858 .expect("Arc object should still be accessible");
4859 let line_obj = frontend
4860 .scene_graph
4861 .objects
4862 .get(line_id.0)
4863 .expect("Line object should still be accessible");
4864
4865 match &arc_obj.kind {
4868 ObjectKind::Segment {
4869 segment: Segment::Arc(_),
4870 } => {}
4871 _ => panic!("Arc object should still be an arc segment"),
4872 }
4873 match &line_obj.kind {
4874 ObjectKind::Segment {
4875 segment: Segment::Line(_),
4876 } => {}
4877 _ => panic!("Line object should still be a line segment"),
4878 }
4879
4880 ctx.close().await;
4881 mock_ctx.close().await;
4882 }
4883
4884 #[tokio::test(flavor = "multi_thread")]
4885 async fn test_distance_two_points() {
4886 let initial_source = "\
4887@settings(experimentalFeatures = allow)
4888
4889sketch(on = XY) {
4890 sketch2::point(at = [var 1, var 2])
4891 sketch2::point(at = [var 3, var 4])
4892}
4893";
4894
4895 let program = Program::parse(initial_source).unwrap().0.unwrap();
4896
4897 let mut frontend = FrontendState::new();
4898
4899 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4900 let mock_ctx = ExecutorContext::new_mock(None).await;
4901 let version = Version(0);
4902
4903 frontend.hack_set_program(&ctx, program).await.unwrap();
4904 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4905 let sketch_id = sketch_object.id;
4906 let sketch = expect_sketch(sketch_object);
4907 let point0_id = *sketch.segments.first().unwrap();
4908 let point1_id = *sketch.segments.get(1).unwrap();
4909
4910 let constraint = Constraint::Distance(Distance {
4911 points: vec![point0_id, point1_id],
4912 distance: Number {
4913 value: 2.0,
4914 units: NumericSuffix::Mm,
4915 },
4916 });
4917 let (src_delta, scene_delta) = frontend
4918 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4919 .await
4920 .unwrap();
4921 assert_eq!(
4922 src_delta.text.as_str(),
4923 "\
4925@settings(experimentalFeatures = allow)
4926
4927sketch(on = XY) {
4928 point1 = sketch2::point(at = [var 1, var 2])
4929 point2 = sketch2::point(at = [var 3, var 4])
4930sketch2::distance([point1, point2]) == 2mm
4931}
4932"
4933 );
4934 assert_eq!(
4935 scene_delta.new_graph.objects.len(),
4936 5,
4937 "{:#?}",
4938 scene_delta.new_graph.objects
4939 );
4940
4941 ctx.close().await;
4942 mock_ctx.close().await;
4943 }
4944
4945 #[tokio::test(flavor = "multi_thread")]
4946 async fn test_horizontal_distance_two_points() {
4947 let initial_source = "\
4948@settings(experimentalFeatures = allow)
4949
4950sketch(on = XY) {
4951 sketch2::point(at = [var 1, var 2])
4952 sketch2::point(at = [var 3, var 4])
4953}
4954";
4955
4956 let program = Program::parse(initial_source).unwrap().0.unwrap();
4957
4958 let mut frontend = FrontendState::new();
4959
4960 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
4961 let mock_ctx = ExecutorContext::new_mock(None).await;
4962 let version = Version(0);
4963
4964 frontend.hack_set_program(&ctx, program).await.unwrap();
4965 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
4966 let sketch_id = sketch_object.id;
4967 let sketch = expect_sketch(sketch_object);
4968 let point0_id = *sketch.segments.first().unwrap();
4969 let point1_id = *sketch.segments.get(1).unwrap();
4970
4971 let constraint = Constraint::HorizontalDistance(Distance {
4972 points: vec![point0_id, point1_id],
4973 distance: Number {
4974 value: 2.0,
4975 units: NumericSuffix::Mm,
4976 },
4977 });
4978 let (src_delta, scene_delta) = frontend
4979 .add_constraint(&mock_ctx, version, sketch_id, constraint)
4980 .await
4981 .unwrap();
4982 assert_eq!(
4983 src_delta.text.as_str(),
4984 "\
4986@settings(experimentalFeatures = allow)
4987
4988sketch(on = XY) {
4989 point1 = sketch2::point(at = [var 1, var 2])
4990 point2 = sketch2::point(at = [var 3, var 4])
4991sketch2::horizontalDistance([point1, point2]) == 2mm
4992}
4993"
4994 );
4995 assert_eq!(
4996 scene_delta.new_graph.objects.len(),
4997 5,
4998 "{:#?}",
4999 scene_delta.new_graph.objects
5000 );
5001
5002 ctx.close().await;
5003 mock_ctx.close().await;
5004 }
5005
5006 #[tokio::test(flavor = "multi_thread")]
5007 async fn test_radius_single_arc_segment() {
5008 let initial_source = "\
5009@settings(experimentalFeatures = allow)
5010
5011sketch(on = XY) {
5012 sketch2::arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
5013}
5014";
5015
5016 let program = Program::parse(initial_source).unwrap().0.unwrap();
5017
5018 let mut frontend = FrontendState::new();
5019
5020 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5021 let mock_ctx = ExecutorContext::new_mock(None).await;
5022 let version = Version(0);
5023
5024 frontend.hack_set_program(&ctx, program).await.unwrap();
5025 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5026 let sketch_id = sketch_object.id;
5027 let sketch = expect_sketch(sketch_object);
5028 let arc_id = sketch
5030 .segments
5031 .iter()
5032 .find(|&seg_id| {
5033 let obj = frontend.scene_graph.objects.get(seg_id.0);
5034 matches!(
5035 obj.map(|o| &o.kind),
5036 Some(ObjectKind::Segment {
5037 segment: Segment::Arc(_)
5038 })
5039 )
5040 })
5041 .unwrap();
5042
5043 let constraint = Constraint::Radius(Radius {
5044 arc: *arc_id,
5045 radius: Number {
5046 value: 5.0,
5047 units: NumericSuffix::Mm,
5048 },
5049 });
5050 let (src_delta, scene_delta) = frontend
5051 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5052 .await
5053 .unwrap();
5054 assert_eq!(
5055 src_delta.text.as_str(),
5056 "\
5058@settings(experimentalFeatures = allow)
5059
5060sketch(on = XY) {
5061 arc1 = sketch2::arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
5062sketch2::radius(arc1) == 5mm
5063}
5064"
5065 );
5066 assert_eq!(
5067 scene_delta.new_graph.objects.len(),
5068 7, "{:#?}",
5070 scene_delta.new_graph.objects
5071 );
5072
5073 ctx.close().await;
5074 mock_ctx.close().await;
5075 }
5076
5077 #[tokio::test(flavor = "multi_thread")]
5078 async fn test_vertical_distance_two_points() {
5079 let initial_source = "\
5080@settings(experimentalFeatures = allow)
5081
5082sketch(on = XY) {
5083 sketch2::point(at = [var 1, var 2])
5084 sketch2::point(at = [var 3, var 4])
5085}
5086";
5087
5088 let program = Program::parse(initial_source).unwrap().0.unwrap();
5089
5090 let mut frontend = FrontendState::new();
5091
5092 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5093 let mock_ctx = ExecutorContext::new_mock(None).await;
5094 let version = Version(0);
5095
5096 frontend.hack_set_program(&ctx, program).await.unwrap();
5097 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5098 let sketch_id = sketch_object.id;
5099 let sketch = expect_sketch(sketch_object);
5100 let point0_id = *sketch.segments.first().unwrap();
5101 let point1_id = *sketch.segments.get(1).unwrap();
5102
5103 let constraint = Constraint::VerticalDistance(Distance {
5104 points: vec![point0_id, point1_id],
5105 distance: Number {
5106 value: 2.0,
5107 units: NumericSuffix::Mm,
5108 },
5109 });
5110 let (src_delta, scene_delta) = frontend
5111 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5112 .await
5113 .unwrap();
5114 assert_eq!(
5115 src_delta.text.as_str(),
5116 "\
5118@settings(experimentalFeatures = allow)
5119
5120sketch(on = XY) {
5121 point1 = sketch2::point(at = [var 1, var 2])
5122 point2 = sketch2::point(at = [var 3, var 4])
5123sketch2::verticalDistance([point1, point2]) == 2mm
5124}
5125"
5126 );
5127 assert_eq!(
5128 scene_delta.new_graph.objects.len(),
5129 5,
5130 "{:#?}",
5131 scene_delta.new_graph.objects
5132 );
5133
5134 ctx.close().await;
5135 mock_ctx.close().await;
5136 }
5137
5138 #[tokio::test(flavor = "multi_thread")]
5139 async fn test_radius_error_cases() {
5140 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5141 let mock_ctx = ExecutorContext::new_mock(None).await;
5142 let version = Version(0);
5143
5144 let initial_source_point = "\
5146@settings(experimentalFeatures = allow)
5147
5148sketch(on = XY) {
5149 sketch2::point(at = [var 1, var 2])
5150}
5151";
5152 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
5153 let mut frontend_point = FrontendState::new();
5154 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
5155 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
5156 let sketch_id_point = sketch_object_point.id;
5157 let sketch_point = expect_sketch(sketch_object_point);
5158 let point_id = *sketch_point.segments.first().unwrap();
5159
5160 let constraint_point = Constraint::Radius(Radius {
5161 arc: point_id,
5162 radius: Number {
5163 value: 5.0,
5164 units: NumericSuffix::Mm,
5165 },
5166 });
5167 let result_point = frontend_point
5168 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
5169 .await;
5170 assert!(result_point.is_err(), "Single point should error for radius");
5171
5172 let initial_source_line = "\
5174@settings(experimentalFeatures = allow)
5175
5176sketch(on = XY) {
5177 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5178}
5179";
5180 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
5181 let mut frontend_line = FrontendState::new();
5182 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
5183 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
5184 let sketch_id_line = sketch_object_line.id;
5185 let sketch_line = expect_sketch(sketch_object_line);
5186 let line_id = *sketch_line.segments.first().unwrap();
5187
5188 let constraint_line = Constraint::Radius(Radius {
5189 arc: line_id,
5190 radius: Number {
5191 value: 5.0,
5192 units: NumericSuffix::Mm,
5193 },
5194 });
5195 let result_line = frontend_line
5196 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
5197 .await;
5198 assert!(result_line.is_err(), "Single line segment should error for radius");
5199
5200 ctx.close().await;
5201 mock_ctx.close().await;
5202 }
5203
5204 #[tokio::test(flavor = "multi_thread")]
5205 async fn test_diameter_single_arc_segment() {
5206 let initial_source = "\
5207@settings(experimentalFeatures = allow)
5208
5209sketch(on = XY) {
5210 sketch2::arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
5211}
5212";
5213
5214 let program = Program::parse(initial_source).unwrap().0.unwrap();
5215
5216 let mut frontend = FrontendState::new();
5217
5218 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5219 let mock_ctx = ExecutorContext::new_mock(None).await;
5220 let version = Version(0);
5221
5222 frontend.hack_set_program(&ctx, program).await.unwrap();
5223 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5224 let sketch_id = sketch_object.id;
5225 let sketch = expect_sketch(sketch_object);
5226 let arc_id = sketch
5228 .segments
5229 .iter()
5230 .find(|&seg_id| {
5231 let obj = frontend.scene_graph.objects.get(seg_id.0);
5232 matches!(
5233 obj.map(|o| &o.kind),
5234 Some(ObjectKind::Segment {
5235 segment: Segment::Arc(_)
5236 })
5237 )
5238 })
5239 .unwrap();
5240
5241 let constraint = Constraint::Diameter(Diameter {
5242 arc: *arc_id,
5243 diameter: Number {
5244 value: 10.0,
5245 units: NumericSuffix::Mm,
5246 },
5247 });
5248 let (src_delta, scene_delta) = frontend
5249 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5250 .await
5251 .unwrap();
5252 assert_eq!(
5253 src_delta.text.as_str(),
5254 "\
5256@settings(experimentalFeatures = allow)
5257
5258sketch(on = XY) {
5259 arc1 = sketch2::arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
5260sketch2::diameter(arc1) == 10mm
5261}
5262"
5263 );
5264 assert_eq!(
5265 scene_delta.new_graph.objects.len(),
5266 7, "{:#?}",
5268 scene_delta.new_graph.objects
5269 );
5270
5271 ctx.close().await;
5272 mock_ctx.close().await;
5273 }
5274
5275 #[tokio::test(flavor = "multi_thread")]
5276 async fn test_diameter_error_cases() {
5277 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5278 let mock_ctx = ExecutorContext::new_mock(None).await;
5279 let version = Version(0);
5280
5281 let initial_source_point = "\
5283@settings(experimentalFeatures = allow)
5284
5285sketch(on = XY) {
5286 sketch2::point(at = [var 1, var 2])
5287}
5288";
5289 let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
5290 let mut frontend_point = FrontendState::new();
5291 frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
5292 let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
5293 let sketch_id_point = sketch_object_point.id;
5294 let sketch_point = expect_sketch(sketch_object_point);
5295 let point_id = *sketch_point.segments.first().unwrap();
5296
5297 let constraint_point = Constraint::Diameter(Diameter {
5298 arc: point_id,
5299 diameter: Number {
5300 value: 10.0,
5301 units: NumericSuffix::Mm,
5302 },
5303 });
5304 let result_point = frontend_point
5305 .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
5306 .await;
5307 assert!(result_point.is_err(), "Single point should error for diameter");
5308
5309 let initial_source_line = "\
5311@settings(experimentalFeatures = allow)
5312
5313sketch(on = XY) {
5314 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5315}
5316";
5317 let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
5318 let mut frontend_line = FrontendState::new();
5319 frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
5320 let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
5321 let sketch_id_line = sketch_object_line.id;
5322 let sketch_line = expect_sketch(sketch_object_line);
5323 let line_id = *sketch_line.segments.first().unwrap();
5324
5325 let constraint_line = Constraint::Diameter(Diameter {
5326 arc: line_id,
5327 diameter: Number {
5328 value: 10.0,
5329 units: NumericSuffix::Mm,
5330 },
5331 });
5332 let result_line = frontend_line
5333 .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
5334 .await;
5335 assert!(result_line.is_err(), "Single line segment should error for diameter");
5336
5337 ctx.close().await;
5338 mock_ctx.close().await;
5339 }
5340
5341 #[tokio::test(flavor = "multi_thread")]
5342 async fn test_line_horizontal() {
5343 let initial_source = "\
5344@settings(experimentalFeatures = allow)
5345
5346sketch(on = XY) {
5347 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5348}
5349";
5350
5351 let program = Program::parse(initial_source).unwrap().0.unwrap();
5352
5353 let mut frontend = FrontendState::new();
5354
5355 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5356 let mock_ctx = ExecutorContext::new_mock(None).await;
5357 let version = Version(0);
5358
5359 frontend.hack_set_program(&ctx, program).await.unwrap();
5360 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5361 let sketch_id = sketch_object.id;
5362 let sketch = expect_sketch(sketch_object);
5363 let line1_id = *sketch.segments.get(2).unwrap();
5364
5365 let constraint = Constraint::Horizontal(Horizontal { line: line1_id });
5366 let (src_delta, scene_delta) = frontend
5367 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5368 .await
5369 .unwrap();
5370 assert_eq!(
5371 src_delta.text.as_str(),
5372 "\
5373@settings(experimentalFeatures = allow)
5374
5375sketch(on = XY) {
5376 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5377 sketch2::horizontal(line1)
5378}
5379"
5380 );
5381 assert_eq!(
5382 scene_delta.new_graph.objects.len(),
5383 6,
5384 "{:#?}",
5385 scene_delta.new_graph.objects
5386 );
5387
5388 ctx.close().await;
5389 mock_ctx.close().await;
5390 }
5391
5392 #[tokio::test(flavor = "multi_thread")]
5393 async fn test_line_vertical() {
5394 let initial_source = "\
5395@settings(experimentalFeatures = allow)
5396
5397sketch(on = XY) {
5398 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5399}
5400";
5401
5402 let program = Program::parse(initial_source).unwrap().0.unwrap();
5403
5404 let mut frontend = FrontendState::new();
5405
5406 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5407 let mock_ctx = ExecutorContext::new_mock(None).await;
5408 let version = Version(0);
5409
5410 frontend.hack_set_program(&ctx, program).await.unwrap();
5411 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5412 let sketch_id = sketch_object.id;
5413 let sketch = expect_sketch(sketch_object);
5414 let line1_id = *sketch.segments.get(2).unwrap();
5415
5416 let constraint = Constraint::Vertical(Vertical { line: line1_id });
5417 let (src_delta, scene_delta) = frontend
5418 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5419 .await
5420 .unwrap();
5421 assert_eq!(
5422 src_delta.text.as_str(),
5423 "\
5424@settings(experimentalFeatures = allow)
5425
5426sketch(on = XY) {
5427 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5428 sketch2::vertical(line1)
5429}
5430"
5431 );
5432 assert_eq!(
5433 scene_delta.new_graph.objects.len(),
5434 6,
5435 "{:#?}",
5436 scene_delta.new_graph.objects
5437 );
5438
5439 ctx.close().await;
5440 mock_ctx.close().await;
5441 }
5442
5443 #[tokio::test(flavor = "multi_thread")]
5444 async fn test_lines_equal_length() {
5445 let initial_source = "\
5446@settings(experimentalFeatures = allow)
5447
5448sketch(on = XY) {
5449 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5450 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5451}
5452";
5453
5454 let program = Program::parse(initial_source).unwrap().0.unwrap();
5455
5456 let mut frontend = FrontendState::new();
5457
5458 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5459 let mock_ctx = ExecutorContext::new_mock(None).await;
5460 let version = Version(0);
5461
5462 frontend.hack_set_program(&ctx, program).await.unwrap();
5463 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5464 let sketch_id = sketch_object.id;
5465 let sketch = expect_sketch(sketch_object);
5466 let line1_id = *sketch.segments.get(2).unwrap();
5467 let line2_id = *sketch.segments.get(5).unwrap();
5468
5469 let constraint = Constraint::LinesEqualLength(LinesEqualLength {
5470 lines: vec![line1_id, line2_id],
5471 });
5472 let (src_delta, scene_delta) = frontend
5473 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5474 .await
5475 .unwrap();
5476 assert_eq!(
5477 src_delta.text.as_str(),
5478 "\
5479@settings(experimentalFeatures = allow)
5480
5481sketch(on = XY) {
5482 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5483 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5484 sketch2::equalLength([line1, line2])
5485}
5486"
5487 );
5488 assert_eq!(
5489 scene_delta.new_graph.objects.len(),
5490 9,
5491 "{:#?}",
5492 scene_delta.new_graph.objects
5493 );
5494
5495 ctx.close().await;
5496 mock_ctx.close().await;
5497 }
5498
5499 #[tokio::test(flavor = "multi_thread")]
5500 async fn test_lines_parallel() {
5501 let initial_source = "\
5502@settings(experimentalFeatures = allow)
5503
5504sketch(on = XY) {
5505 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5506 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5507}
5508";
5509
5510 let program = Program::parse(initial_source).unwrap().0.unwrap();
5511
5512 let mut frontend = FrontendState::new();
5513
5514 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5515 let mock_ctx = ExecutorContext::new_mock(None).await;
5516 let version = Version(0);
5517
5518 frontend.hack_set_program(&ctx, program).await.unwrap();
5519 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5520 let sketch_id = sketch_object.id;
5521 let sketch = expect_sketch(sketch_object);
5522 let line1_id = *sketch.segments.get(2).unwrap();
5523 let line2_id = *sketch.segments.get(5).unwrap();
5524
5525 let constraint = Constraint::Parallel(Parallel {
5526 lines: vec![line1_id, line2_id],
5527 });
5528 let (src_delta, scene_delta) = frontend
5529 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5530 .await
5531 .unwrap();
5532 assert_eq!(
5533 src_delta.text.as_str(),
5534 "\
5535@settings(experimentalFeatures = allow)
5536
5537sketch(on = XY) {
5538 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5539 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5540 sketch2::parallel([line1, line2])
5541}
5542"
5543 );
5544 assert_eq!(
5545 scene_delta.new_graph.objects.len(),
5546 9,
5547 "{:#?}",
5548 scene_delta.new_graph.objects
5549 );
5550
5551 ctx.close().await;
5552 mock_ctx.close().await;
5553 }
5554
5555 #[tokio::test(flavor = "multi_thread")]
5556 async fn test_lines_perpendicular() {
5557 let initial_source = "\
5558@settings(experimentalFeatures = allow)
5559
5560sketch(on = XY) {
5561 sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5562 sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5563}
5564";
5565
5566 let program = Program::parse(initial_source).unwrap().0.unwrap();
5567
5568 let mut frontend = FrontendState::new();
5569
5570 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5571 let mock_ctx = ExecutorContext::new_mock(None).await;
5572 let version = Version(0);
5573
5574 frontend.hack_set_program(&ctx, program).await.unwrap();
5575 let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
5576 let sketch_id = sketch_object.id;
5577 let sketch = expect_sketch(sketch_object);
5578 let line1_id = *sketch.segments.get(2).unwrap();
5579 let line2_id = *sketch.segments.get(5).unwrap();
5580
5581 let constraint = Constraint::Perpendicular(Perpendicular {
5582 lines: vec![line1_id, line2_id],
5583 });
5584 let (src_delta, scene_delta) = frontend
5585 .add_constraint(&mock_ctx, version, sketch_id, constraint)
5586 .await
5587 .unwrap();
5588 assert_eq!(
5589 src_delta.text.as_str(),
5590 "\
5591@settings(experimentalFeatures = allow)
5592
5593sketch(on = XY) {
5594 line1 = sketch2::line(start = [var 1, var 2], end = [var 3, var 4])
5595 line2 = sketch2::line(start = [var 5, var 6], end = [var 7, var 8])
5596 sketch2::perpendicular([line1, line2])
5597}
5598"
5599 );
5600 assert_eq!(
5601 scene_delta.new_graph.objects.len(),
5602 9,
5603 "{:#?}",
5604 scene_delta.new_graph.objects
5605 );
5606
5607 ctx.close().await;
5608 mock_ctx.close().await;
5609 }
5610
5611 #[tokio::test(flavor = "multi_thread")]
5612 async fn test_sketch_on_face_simple() {
5613 let initial_source = "\
5614@settings(experimentalFeatures = allow)
5615
5616len = 2mm
5617cube = startSketchOn(XY)
5618 |> startProfile(at = [0, 0])
5619 |> line(end = [len, 0], tag = $side)
5620 |> line(end = [0, len])
5621 |> line(end = [-len, 0])
5622 |> line(end = [0, -len])
5623 |> close()
5624 |> extrude(length = len)
5625
5626face = faceOf(cube, face = side)
5627";
5628
5629 let program = Program::parse(initial_source).unwrap().0.unwrap();
5630
5631 let mut frontend = FrontendState::new();
5632
5633 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5634 let mock_ctx = ExecutorContext::new_mock(None).await;
5635 let version = Version(0);
5636
5637 frontend.hack_set_program(&ctx, program).await.unwrap();
5638 let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
5639 let face_id = face_object.id;
5640
5641 let sketch_args = SketchCtor { on: "face".to_owned() };
5642 let (_src_delta, scene_delta, sketch_id) = frontend
5643 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
5644 .await
5645 .unwrap();
5646 assert_eq!(sketch_id, ObjectId(2));
5647 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
5648 let sketch_object = &scene_delta.new_graph.objects[2];
5649 assert_eq!(sketch_object.id, ObjectId(2));
5650 assert_eq!(
5651 sketch_object.kind,
5652 ObjectKind::Sketch(Sketch {
5653 args: SketchCtor { on: "face".to_owned() },
5654 plane: face_id,
5655 segments: vec![],
5656 constraints: vec![],
5657 })
5658 );
5659 assert_eq!(scene_delta.new_graph.objects.len(), 3);
5660
5661 ctx.close().await;
5662 mock_ctx.close().await;
5663 }
5664
5665 #[tokio::test(flavor = "multi_thread")]
5666 async fn test_sketch_on_plane_incremental() {
5667 let initial_source = "\
5668@settings(experimentalFeatures = allow)
5669
5670len = 2mm
5671cube = startSketchOn(XY)
5672 |> startProfile(at = [0, 0])
5673 |> line(end = [len, 0], tag = $side)
5674 |> line(end = [0, len])
5675 |> line(end = [-len, 0])
5676 |> line(end = [0, -len])
5677 |> close()
5678 |> extrude(length = len)
5679
5680plane = planeOf(cube, face = side)
5681";
5682
5683 let program = Program::parse(initial_source).unwrap().0.unwrap();
5684
5685 let mut frontend = FrontendState::new();
5686
5687 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5688 let mock_ctx = ExecutorContext::new_mock(None).await;
5689 let version = Version(0);
5690
5691 frontend.hack_set_program(&ctx, program).await.unwrap();
5692 let plane_object = frontend
5694 .scene_graph
5695 .objects
5696 .iter()
5697 .rev()
5698 .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
5699 .unwrap();
5700 let plane_id = plane_object.id;
5701
5702 let sketch_args = SketchCtor { on: "plane".to_owned() };
5703 let (src_delta, scene_delta, sketch_id) = frontend
5704 .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
5705 .await
5706 .unwrap();
5707 assert_eq!(
5708 src_delta.text.as_str(),
5709 "\
5710@settings(experimentalFeatures = allow)
5711
5712len = 2mm
5713cube = startSketchOn(XY)
5714 |> startProfile(at = [0, 0])
5715 |> line(end = [len, 0], tag = $side)
5716 |> line(end = [0, len])
5717 |> line(end = [-len, 0])
5718 |> line(end = [0, -len])
5719 |> close()
5720 |> extrude(length = len)
5721
5722plane = planeOf(cube, face = side)
5723sketch(on = plane) {
5724}
5725"
5726 );
5727 assert_eq!(sketch_id, ObjectId(2));
5728 assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
5729 let sketch_object = &scene_delta.new_graph.objects[2];
5730 assert_eq!(sketch_object.id, ObjectId(2));
5731 assert_eq!(
5732 sketch_object.kind,
5733 ObjectKind::Sketch(Sketch {
5734 args: SketchCtor { on: "plane".to_owned() },
5735 plane: plane_id,
5736 segments: vec![],
5737 constraints: vec![],
5738 })
5739 );
5740 assert_eq!(scene_delta.new_graph.objects.len(), 3);
5741
5742 let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
5743 assert_eq!(plane_object.id, plane_id);
5744 assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
5745
5746 ctx.close().await;
5747 mock_ctx.close().await;
5748 }
5749
5750 #[tokio::test(flavor = "multi_thread")]
5751 async fn test_multiple_sketch_blocks() {
5752 let initial_source = "\
5753@settings(experimentalFeatures = allow)
5754
5755// Cube that requires the engine.
5756width = 2
5757sketch001 = startSketchOn(XY)
5758profile001 = startProfile(sketch001, at = [0, 0])
5759 |> yLine(length = width, tag = $seg1)
5760 |> xLine(length = width)
5761 |> yLine(length = -width)
5762 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5763 |> close()
5764extrude001 = extrude(profile001, length = width)
5765
5766// Get a value that requires the engine.
5767x = segLen(seg1)
5768
5769// Triangle with side length 2*x.
5770sketch(on = XY) {
5771 line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
5772 line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
5773 sketch2::coincident([line1.end, line2.start])
5774 line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
5775 sketch2::coincident([line2.end, line3.start])
5776 sketch2::coincident([line3.end, line1.start])
5777 sketch2::equalLength([line3, line1])
5778 sketch2::equalLength([line1, line2])
5779sketch2::distance([line1.start, line1.end]) == 2*x
5780}
5781
5782// Line segment with length x.
5783sketch2 = sketch(on = XY) {
5784 line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
5785sketch2::distance([line1.start, line1.end]) == x
5786}
5787";
5788
5789 let program = Program::parse(initial_source).unwrap().0.unwrap();
5790
5791 let mut frontend = FrontendState::new();
5792
5793 let ctx = ExecutorContext::new_with_default_client().await.unwrap();
5794 let mock_ctx = ExecutorContext::new_mock(None).await;
5795 let version = Version(0);
5796 let project_id = ProjectId(0);
5797 let file_id = FileId(0);
5798
5799 frontend.hack_set_program(&ctx, program).await.unwrap();
5800 let sketch_objects = frontend
5801 .scene_graph
5802 .objects
5803 .iter()
5804 .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
5805 .collect::<Vec<_>>();
5806 let sketch1_id = sketch_objects.first().unwrap().id;
5807 let sketch2_id = sketch_objects.get(1).unwrap().id;
5808 let point1_id = ObjectId(sketch1_id.0 + 1);
5810 let point2_id = ObjectId(sketch2_id.0 + 1);
5812
5813 let scene_delta = frontend
5822 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
5823 .await
5824 .unwrap();
5825 assert_eq!(
5826 scene_delta.new_graph.objects.len(),
5827 18,
5828 "{:#?}",
5829 scene_delta.new_graph.objects
5830 );
5831
5832 let point_ctor = PointCtor {
5834 position: Point2d {
5835 x: Expr::Var(Number {
5836 value: 1.0,
5837 units: NumericSuffix::Mm,
5838 }),
5839 y: Expr::Var(Number {
5840 value: 2.0,
5841 units: NumericSuffix::Mm,
5842 }),
5843 },
5844 };
5845 let segments = vec![ExistingSegmentCtor {
5846 id: point1_id,
5847 ctor: SegmentCtor::Point(point_ctor),
5848 }];
5849 let (src_delta, _) = frontend
5850 .edit_segments(&mock_ctx, version, sketch1_id, segments)
5851 .await
5852 .unwrap();
5853 assert_eq!(
5855 src_delta.text.as_str(),
5856 "\
5857@settings(experimentalFeatures = allow)
5858
5859// Cube that requires the engine.
5860width = 2
5861sketch001 = startSketchOn(XY)
5862profile001 = startProfile(sketch001, at = [0, 0])
5863 |> yLine(length = width, tag = $seg1)
5864 |> xLine(length = width)
5865 |> yLine(length = -width)
5866 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5867 |> close()
5868extrude001 = extrude(profile001, length = width)
5869
5870// Get a value that requires the engine.
5871x = segLen(seg1)
5872
5873// Triangle with side length 2*x.
5874sketch(on = XY) {
5875 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 2.32mm, var -1.78mm])
5876 line2 = sketch2::line(start = [var 2.32mm, var -1.78mm], end = [var -1.61mm, var -1.03mm])
5877 sketch2::coincident([line1.end, line2.start])
5878 line3 = sketch2::line(start = [var -1.61mm, var -1.03mm], end = [var 1mm, var 2mm])
5879 sketch2::coincident([line2.end, line3.start])
5880 sketch2::coincident([line3.end, line1.start])
5881 sketch2::equalLength([line3, line1])
5882 sketch2::equalLength([line1, line2])
5883sketch2::distance([line1.start, line1.end]) == 2 * x
5884}
5885
5886// Line segment with length x.
5887sketch2 = sketch(on = XY) {
5888 line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
5889sketch2::distance([line1.start, line1.end]) == x
5890}
5891"
5892 );
5893
5894 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
5896 assert_eq!(
5898 src_delta.text.as_str(),
5899 "\
5900@settings(experimentalFeatures = allow)
5901
5902// Cube that requires the engine.
5903width = 2
5904sketch001 = startSketchOn(XY)
5905profile001 = startProfile(sketch001, at = [0, 0])
5906 |> yLine(length = width, tag = $seg1)
5907 |> xLine(length = width)
5908 |> yLine(length = -width)
5909 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5910 |> close()
5911extrude001 = extrude(profile001, length = width)
5912
5913// Get a value that requires the engine.
5914x = segLen(seg1)
5915
5916// Triangle with side length 2*x.
5917sketch(on = XY) {
5918 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
5919 line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
5920 sketch2::coincident([line1.end, line2.start])
5921 line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
5922 sketch2::coincident([line2.end, line3.start])
5923 sketch2::coincident([line3.end, line1.start])
5924 sketch2::equalLength([line3, line1])
5925 sketch2::equalLength([line1, line2])
5926sketch2::distance([line1.start, line1.end]) == 2 * x
5927}
5928
5929// Line segment with length x.
5930sketch2 = sketch(on = XY) {
5931 line1 = sketch2::line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
5932sketch2::distance([line1.start, line1.end]) == x
5933}
5934"
5935 );
5936 let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
5944 assert_eq!(scene.objects.len(), 23, "{:#?}", scene.objects);
5945
5946 let scene_delta = frontend
5954 .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
5955 .await
5956 .unwrap();
5957 assert_eq!(
5958 scene_delta.new_graph.objects.len(),
5959 23,
5960 "{:#?}",
5961 scene_delta.new_graph.objects
5962 );
5963
5964 let point_ctor = PointCtor {
5966 position: Point2d {
5967 x: Expr::Var(Number {
5968 value: 3.0,
5969 units: NumericSuffix::Mm,
5970 }),
5971 y: Expr::Var(Number {
5972 value: 4.0,
5973 units: NumericSuffix::Mm,
5974 }),
5975 },
5976 };
5977 let segments = vec![ExistingSegmentCtor {
5978 id: point2_id,
5979 ctor: SegmentCtor::Point(point_ctor),
5980 }];
5981 let (src_delta, _) = frontend
5982 .edit_segments(&mock_ctx, version, sketch2_id, segments)
5983 .await
5984 .unwrap();
5985 assert_eq!(
5987 src_delta.text.as_str(),
5988 "\
5989@settings(experimentalFeatures = allow)
5990
5991// Cube that requires the engine.
5992width = 2
5993sketch001 = startSketchOn(XY)
5994profile001 = startProfile(sketch001, at = [0, 0])
5995 |> yLine(length = width, tag = $seg1)
5996 |> xLine(length = width)
5997 |> yLine(length = -width)
5998 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
5999 |> close()
6000extrude001 = extrude(profile001, length = width)
6001
6002// Get a value that requires the engine.
6003x = segLen(seg1)
6004
6005// Triangle with side length 2*x.
6006sketch(on = XY) {
6007 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
6008 line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
6009 sketch2::coincident([line1.end, line2.start])
6010 line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
6011 sketch2::coincident([line2.end, line3.start])
6012 sketch2::coincident([line3.end, line1.start])
6013 sketch2::equalLength([line3, line1])
6014 sketch2::equalLength([line1, line2])
6015sketch2::distance([line1.start, line1.end]) == 2 * x
6016}
6017
6018// Line segment with length x.
6019sketch2 = sketch(on = XY) {
6020 line1 = sketch2::line(start = [var 3mm, var 4mm], end = [var 2.32mm, var 2.12mm])
6021sketch2::distance([line1.start, line1.end]) == x
6022}
6023"
6024 );
6025
6026 let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
6028 assert_eq!(
6030 src_delta.text.as_str(),
6031 "\
6032@settings(experimentalFeatures = allow)
6033
6034// Cube that requires the engine.
6035width = 2
6036sketch001 = startSketchOn(XY)
6037profile001 = startProfile(sketch001, at = [0, 0])
6038 |> yLine(length = width, tag = $seg1)
6039 |> xLine(length = width)
6040 |> yLine(length = -width)
6041 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
6042 |> close()
6043extrude001 = extrude(profile001, length = width)
6044
6045// Get a value that requires the engine.
6046x = segLen(seg1)
6047
6048// Triangle with side length 2*x.
6049sketch(on = XY) {
6050 line1 = sketch2::line(start = [var 1mm, var 2mm], end = [var 1.28mm, var -0.78mm])
6051 line2 = sketch2::line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
6052 sketch2::coincident([line1.end, line2.start])
6053 line3 = sketch2::line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
6054 sketch2::coincident([line2.end, line3.start])
6055 sketch2::coincident([line3.end, line1.start])
6056 sketch2::equalLength([line3, line1])
6057 sketch2::equalLength([line1, line2])
6058sketch2::distance([line1.start, line1.end]) == 2 * x
6059}
6060
6061// Line segment with length x.
6062sketch2 = sketch(on = XY) {
6063 line1 = sketch2::line(start = [var 3mm, var 4mm], end = [var 1.28mm, var -0.78mm])
6064sketch2::distance([line1.start, line1.end]) == x
6065}
6066"
6067 );
6068
6069 ctx.close().await;
6070 mock_ctx.close().await;
6071 }
6072}